fix: allow adding contacts from mail recipient popover on mobile #306
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useRouter } from "@/i18n/navigation";
|
||||||
import { ArrowLeft, Users, AlertTriangle } from "lucide-react";
|
import { ArrowLeft, Users, AlertTriangle } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||||
@@ -93,6 +95,8 @@ export default function ContactsPage() {
|
|||||||
const [renamingAddressBook, setRenamingAddressBook] = useState<AddressBook | null>(null);
|
const [renamingAddressBook, setRenamingAddressBook] = useState<AddressBook | null>(null);
|
||||||
const [sharingAddressBookId, setSharingAddressBookId] = useState<string | null>(null);
|
const [sharingAddressBookId, setSharingAddressBookId] = useState<string | null>(null);
|
||||||
const [defaultBookIdForCreate, setDefaultBookIdForCreate] = useState<string | undefined>(undefined);
|
const [defaultBookIdForCreate, setDefaultBookIdForCreate] = useState<string | undefined>(undefined);
|
||||||
|
const [createPrefill, setCreatePrefill] = useState<{ email?: string; name?: string } | undefined>(undefined);
|
||||||
|
const [returnToEmail, setReturnToEmail] = useState(false);
|
||||||
const [renamingKeyword, setRenamingKeyword] = useState<string | null>(null);
|
const [renamingKeyword, setRenamingKeyword] = useState<string | null>(null);
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
||||||
const hasFetched = useRef(false);
|
const hasFetched = useRef(false);
|
||||||
@@ -100,6 +104,12 @@ export default function ContactsPage() {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isDesktop = useIsDesktop();
|
const isDesktop = useIsDesktop();
|
||||||
const isEmbedded = useIsEmbedded();
|
const isEmbedded = useIsEmbedded();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
// One-shot intent flag: only consume the URL params on the first render that
|
||||||
|
// has them. After applying, we strip the query so a later refresh or
|
||||||
|
// re-mount doesn't re-trigger the navigation.
|
||||||
|
const intentAppliedRef = useRef(false);
|
||||||
// Narrow pane (Pro split or small window): the categories sidebar collapses
|
// Narrow pane (Pro split or small window): the categories sidebar collapses
|
||||||
// into a burger-toggled overlay.
|
// into a burger-toggled overlay.
|
||||||
const isNarrow = !isDesktop;
|
const isNarrow = !isDesktop;
|
||||||
@@ -154,6 +164,29 @@ export default function ContactsPage() {
|
|||||||
}
|
}
|
||||||
}, [client, supportsSync, fetchContacts, isEmbedded]);
|
}, [client, supportsSync, fetchContacts, isEmbedded]);
|
||||||
|
|
||||||
|
// Consume one-shot URL params (set by the mobile recipient popover when no
|
||||||
|
// sidebar is available) and strip them so a refresh doesn't replay the
|
||||||
|
// intent. `from=email` flips the mobile back button to `router.back()`.
|
||||||
|
useEffect(() => {
|
||||||
|
if (intentAppliedRef.current) return;
|
||||||
|
const contactId = searchParams.get('contactId');
|
||||||
|
const addEmail = searchParams.get('addEmail');
|
||||||
|
const addName = searchParams.get('addName');
|
||||||
|
const from = searchParams.get('from');
|
||||||
|
if (!contactId && !addEmail && !from) return;
|
||||||
|
intentAppliedRef.current = true;
|
||||||
|
if (from === 'email') setReturnToEmail(true);
|
||||||
|
if (contactId) {
|
||||||
|
setSelectedContact(contactId);
|
||||||
|
setView('detail');
|
||||||
|
} else if (addEmail) {
|
||||||
|
setCreatePrefill({ email: addEmail, name: addName ?? undefined });
|
||||||
|
setSelectedContact(null);
|
||||||
|
setView('create');
|
||||||
|
}
|
||||||
|
router.replace('/contacts');
|
||||||
|
}, [searchParams, router, setSelectedContact]);
|
||||||
|
|
||||||
// Intercept browser refresh gestures (F5, Ctrl/Cmd+R, pull-to-refresh)
|
// Intercept browser refresh gestures (F5, Ctrl/Cmd+R, pull-to-refresh)
|
||||||
// and refresh contacts via JMAP instead of reloading the page.
|
// and refresh contacts via JMAP instead of reloading the page.
|
||||||
useRefreshGesture({
|
useRefreshGesture({
|
||||||
@@ -365,8 +398,14 @@ export default function ContactsPage() {
|
|||||||
toast.success(t("toast.created"));
|
toast.success(t("toast.created"));
|
||||||
}
|
}
|
||||||
setDefaultBookIdForCreate(undefined);
|
setDefaultBookIdForCreate(undefined);
|
||||||
|
setCreatePrefill(undefined);
|
||||||
|
if (returnToEmail) {
|
||||||
|
setReturnToEmail(false);
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setView("list");
|
setView("list");
|
||||||
}, [supportsSync, client, createContact, addLocalContact, t]);
|
}, [supportsSync, client, createContact, addLocalContact, t, returnToEmail, router]);
|
||||||
|
|
||||||
const handleSaveEdit = useCallback(async (data: Partial<ContactCard>) => {
|
const handleSaveEdit = useCallback(async (data: Partial<ContactCard>) => {
|
||||||
if (!selectedContact) return;
|
if (!selectedContact) return;
|
||||||
@@ -383,6 +422,14 @@ export default function ContactsPage() {
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setDefaultBookIdForCreate(undefined);
|
setDefaultBookIdForCreate(undefined);
|
||||||
|
// Came from email → cancel returns to the email instead of the contact list.
|
||||||
|
if (returnToEmail && view === "create") {
|
||||||
|
setCreatePrefill(undefined);
|
||||||
|
setReturnToEmail(false);
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (view === "create") setCreatePrefill(undefined);
|
||||||
if (view === "group-create" || view === "group-edit") {
|
if (view === "group-create" || view === "group-edit") {
|
||||||
setView(selectedGroup ? "group-detail" : "list");
|
setView(selectedGroup ? "group-detail" : "list");
|
||||||
} else if (view === "bulk-add-to-group") {
|
} else if (view === "bulk-add-to-group") {
|
||||||
@@ -554,7 +601,7 @@ export default function ContactsPage() {
|
|||||||
const renderRightPanel = () => {
|
const renderRightPanel = () => {
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case "create":
|
case "create":
|
||||||
return <ContactForm addressBooks={addressBooks} allKeywords={allKeywords} defaultAddressBookId={defaultBookIdForCreate} onSave={handleSaveNew} onCancel={handleCancel} />;
|
return <ContactForm addressBooks={addressBooks} allKeywords={allKeywords} defaultAddressBookId={defaultBookIdForCreate} prefill={createPrefill} onSave={handleSaveNew} onCancel={handleCancel} />;
|
||||||
|
|
||||||
case "edit":
|
case "edit":
|
||||||
if (!selectedContact) return null;
|
if (!selectedContact) return null;
|
||||||
@@ -686,6 +733,12 @@ export default function ContactsPage() {
|
|||||||
const showRightPanel = !isMobile || view !== "list";
|
const showRightPanel = !isMobile || view !== "list";
|
||||||
|
|
||||||
const mobileBackToList = () => {
|
const mobileBackToList = () => {
|
||||||
|
if (returnToEmail) {
|
||||||
|
setReturnToEmail(false);
|
||||||
|
setCreatePrefill(undefined);
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setView("list");
|
setView("list");
|
||||||
clearSelection();
|
clearSelection();
|
||||||
};
|
};
|
||||||
@@ -853,7 +906,7 @@ export default function ContactsPage() {
|
|||||||
className="touch-manipulation"
|
className="touch-manipulation"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
{t("back_to_contacts")}
|
{returnToEmail ? t("back_to_email") : t("back_to_contacts")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ interface ContactFormProps {
|
|||||||
addressBooks?: AddressBook[];
|
addressBooks?: AddressBook[];
|
||||||
allKeywords?: string[];
|
allKeywords?: string[];
|
||||||
defaultAddressBookId?: string;
|
defaultAddressBookId?: string;
|
||||||
|
/** Prefills the create form (ignored when `contact` is set). */
|
||||||
|
prefill?: { email?: string; name?: string };
|
||||||
onSave: (data: Partial<ContactCard>) => Promise<void>;
|
onSave: (data: Partial<ContactCard>) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
@@ -145,10 +147,22 @@ function Select({ value, onChange, children, className }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContactForm({ contact, addressBooks, allKeywords, defaultAddressBookId, onSave, onCancel }: ContactFormProps) {
|
export function ContactForm({ contact, addressBooks, allKeywords, defaultAddressBookId, prefill, onSave, onCancel }: ContactFormProps) {
|
||||||
const t = useTranslations("contacts.form");
|
const t = useTranslations("contacts.form");
|
||||||
const isEditing = !!contact;
|
const isEditing = !!contact;
|
||||||
|
|
||||||
|
// Split a free-form display name into given/surname for prefill.
|
||||||
|
const prefillGivenName = (() => {
|
||||||
|
if (contact || !prefill?.name) return "";
|
||||||
|
const parts = prefill.name.trim().split(/\s+/);
|
||||||
|
return parts[0] || "";
|
||||||
|
})();
|
||||||
|
const prefillSurname = (() => {
|
||||||
|
if (contact || !prefill?.name) return "";
|
||||||
|
const parts = prefill.name.trim().split(/\s+/);
|
||||||
|
return parts.slice(1).join(" ");
|
||||||
|
})();
|
||||||
|
|
||||||
// Accept JSContact-standard kinds (RFC 9553) and legacy vCard-style aliases.
|
// Accept JSContact-standard kinds (RFC 9553) and legacy vCard-style aliases.
|
||||||
const findComponent = (...kinds: string[]) =>
|
const findComponent = (...kinds: string[]) =>
|
||||||
contact?.name?.components?.find(c => kinds.includes(c.kind))?.value || "";
|
contact?.name?.components?.find(c => kinds.includes(c.kind))?.value || "";
|
||||||
@@ -215,9 +229,9 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [prefix, setPrefix] = useState(findComponent("title", "prefix"));
|
const [prefix, setPrefix] = useState(findComponent("title", "prefix"));
|
||||||
const [givenName, setGivenName] = useState(findComponent("given"));
|
const [givenName, setGivenName] = useState(findComponent("given") || prefillGivenName);
|
||||||
const [additionalName, setAdditionalName] = useState(findComponent("given2", "additional", "middle"));
|
const [additionalName, setAdditionalName] = useState(findComponent("given2", "additional", "middle"));
|
||||||
const [surname, setSurname] = useState(findComponent("surname"));
|
const [surname, setSurname] = useState(findComponent("surname") || prefillSurname);
|
||||||
const [suffix, setSuffix] = useState(findComponent("generation", "suffix"));
|
const [suffix, setSuffix] = useState(findComponent("generation", "suffix"));
|
||||||
|
|
||||||
const [nickname, setNickname] = useState(
|
const [nickname, setNickname] = useState(
|
||||||
@@ -231,7 +245,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
context: e.contexts?.work ? "work" : e.contexts?.private ? "private" : "",
|
context: e.contexts?.work ? "work" : e.contexts?.private ? "private" : "",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return [{ address: "", context: "" }];
|
return [{ address: prefill?.email || "", context: "" }];
|
||||||
});
|
});
|
||||||
|
|
||||||
const [phones, setPhones] = useState<PhoneEntry[]>(() => {
|
const [phones, setPhones] = useState<PhoneEntry[]>(() => {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import {
|
|||||||
PenSquare,
|
PenSquare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "@/i18n/navigation";
|
||||||
import type { Attachment as PostalMimeAttachment } from 'postal-mime';
|
import type { Attachment as PostalMimeAttachment } from 'postal-mime';
|
||||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||||
import { useUIStore } from "@/stores/ui-store";
|
import { useUIStore } from "@/stores/ui-store";
|
||||||
@@ -1137,9 +1138,34 @@ export function EmailViewer({
|
|||||||
const [contactSidebarEmail, setContactSidebarEmail] = useState<string | null>(null);
|
const [contactSidebarEmail, setContactSidebarEmail] = useState<string | null>(null);
|
||||||
const contacts = useContactStore((s) => s.contacts);
|
const contacts = useContactStore((s) => s.contacts);
|
||||||
const { isMobile: isMobileDevice } = useDeviceDetection();
|
const { isMobile: isMobileDevice } = useDeviceDetection();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleViewContactSidebar = (contact: ContactCard | null, recipientEmail: string) => {
|
const handleViewContactSidebar = (contact: ContactCard | null, recipientEmail: string) => {
|
||||||
if (isMobileDevice) return; // no sidebar on mobile
|
if (isMobileDevice) {
|
||||||
|
// No room for a sidebar on mobile — send the user to the contacts page
|
||||||
|
// with params describing what to show. The `from=email` flag turns the
|
||||||
|
// page's mobile back button into a router.back() that returns here.
|
||||||
|
const allRecipients = [
|
||||||
|
...(email?.from || []),
|
||||||
|
...(email?.to || []),
|
||||||
|
...(email?.cc || []),
|
||||||
|
...(email?.bcc || []),
|
||||||
|
...(email?.replyTo || []),
|
||||||
|
];
|
||||||
|
const recipientName = allRecipients.find(
|
||||||
|
(r) => r.email.toLowerCase() === recipientEmail.toLowerCase()
|
||||||
|
)?.name;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (contact) {
|
||||||
|
params.set('contactId', contact.id);
|
||||||
|
} else {
|
||||||
|
params.set('addEmail', recipientEmail);
|
||||||
|
if (recipientName) params.set('addName', recipientName);
|
||||||
|
}
|
||||||
|
params.set('from', 'email');
|
||||||
|
router.push(`/contacts?${params.toString()}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setContactSidebarEmail(recipientEmail);
|
setContactSidebarEmail(recipientEmail);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Opravdu chcete odstranit tento kontakt?",
|
"delete_confirm": "Opravdu chcete odstranit tento kontakt?",
|
||||||
"local_mode": "Kontakty jsou uloženy lokálně (server nepodporuje JMAP Contacts)",
|
"local_mode": "Kontakty jsou uloženy lokálně (server nepodporuje JMAP Contacts)",
|
||||||
"back_to_contacts": "Zpět na kontakty",
|
"back_to_contacts": "Zpět na kontakty",
|
||||||
|
"back_to_email": "Zpět na e-mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Všechny",
|
"all": "Všechny",
|
||||||
"groups": "Skupiny"
|
"groups": "Skupiny"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Er du sikker på, at du vil slette denne kontakt?",
|
"delete_confirm": "Er du sikker på, at du vil slette denne kontakt?",
|
||||||
"local_mode": "Kontakter gemmes lokalt (serveren understøtter ikke JMAP-kontakter)",
|
"local_mode": "Kontakter gemmes lokalt (serveren understøtter ikke JMAP-kontakter)",
|
||||||
"back_to_contacts": "Tilbage til kontakter",
|
"back_to_contacts": "Tilbage til kontakter",
|
||||||
|
"back_to_email": "Tilbage til e-mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"groups": "Grupper"
|
"groups": "Grupper"
|
||||||
|
|||||||
@@ -1962,6 +1962,7 @@
|
|||||||
"delete_confirm": "Möchten Sie diesen Kontakt wirklich löschen?",
|
"delete_confirm": "Möchten Sie diesen Kontakt wirklich löschen?",
|
||||||
"local_mode": "Kontakte werden lokal gespeichert (Server unterstützt kein JMAP Contacts)",
|
"local_mode": "Kontakte werden lokal gespeichert (Server unterstützt kein JMAP Contacts)",
|
||||||
"back_to_contacts": "Zurück zu Kontakten",
|
"back_to_contacts": "Zurück zu Kontakten",
|
||||||
|
"back_to_email": "Zurück zur E-Mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"groups": "Gruppen"
|
"groups": "Gruppen"
|
||||||
|
|||||||
@@ -1962,6 +1962,7 @@
|
|||||||
"delete_confirm": "Are you sure you want to delete this contact?",
|
"delete_confirm": "Are you sure you want to delete this contact?",
|
||||||
"local_mode": "Contacts are stored locally (server does not support JMAP Contacts)",
|
"local_mode": "Contacts are stored locally (server does not support JMAP Contacts)",
|
||||||
"back_to_contacts": "Back to contacts",
|
"back_to_contacts": "Back to contacts",
|
||||||
|
"back_to_email": "Back to email",
|
||||||
"open_categories": "Open categories",
|
"open_categories": "Open categories",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "¿Estás seguro de que quieres eliminar este contacto?",
|
"delete_confirm": "¿Estás seguro de que quieres eliminar este contacto?",
|
||||||
"local_mode": "Los contactos se almacenan localmente (el servidor no soporta JMAP Contacts)",
|
"local_mode": "Los contactos se almacenan localmente (el servidor no soporta JMAP Contacts)",
|
||||||
"back_to_contacts": "Volver a contactos",
|
"back_to_contacts": "Volver a contactos",
|
||||||
|
"back_to_email": "Volver al correo",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Todos",
|
"all": "Todos",
|
||||||
"groups": "Grupos"
|
"groups": "Grupos"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ce contact ?",
|
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ce contact ?",
|
||||||
"local_mode": "Les contacts sont stockés localement (le serveur ne prend pas en charge JMAP Contacts)",
|
"local_mode": "Les contacts sont stockés localement (le serveur ne prend pas en charge JMAP Contacts)",
|
||||||
"back_to_contacts": "Retour aux contacts",
|
"back_to_contacts": "Retour aux contacts",
|
||||||
|
"back_to_email": "Retour à l'e-mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Tous",
|
"all": "Tous",
|
||||||
"groups": "Groupes"
|
"groups": "Groupes"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Sei sicuro di voler eliminare questo contatto?",
|
"delete_confirm": "Sei sicuro di voler eliminare questo contatto?",
|
||||||
"local_mode": "I contatti sono salvati localmente (il server non supporta JMAP Contacts)",
|
"local_mode": "I contatti sono salvati localmente (il server non supporta JMAP Contacts)",
|
||||||
"back_to_contacts": "Torna ai contatti",
|
"back_to_contacts": "Torna ai contatti",
|
||||||
|
"back_to_email": "Torna all'e-mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Tutti",
|
"all": "Tutti",
|
||||||
"groups": "Gruppi"
|
"groups": "Gruppi"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "この連絡先を削除してもよろしいですか?",
|
"delete_confirm": "この連絡先を削除してもよろしいですか?",
|
||||||
"local_mode": "連絡先はローカルに保存されています(サーバーがJMAPコンタクトをサポートしていません)",
|
"local_mode": "連絡先はローカルに保存されています(サーバーがJMAPコンタクトをサポートしていません)",
|
||||||
"back_to_contacts": "連絡先に戻る",
|
"back_to_contacts": "連絡先に戻る",
|
||||||
|
"back_to_email": "メールに戻る",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "すべて",
|
"all": "すべて",
|
||||||
"groups": "グループ"
|
"groups": "グループ"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "정말 이 연락처를 삭제할까요?",
|
"delete_confirm": "정말 이 연락처를 삭제할까요?",
|
||||||
"local_mode": "연락처가 로컬에 저장돼요 (서버가 JMAP Contacts를 지원하지 않아요)",
|
"local_mode": "연락처가 로컬에 저장돼요 (서버가 JMAP Contacts를 지원하지 않아요)",
|
||||||
"back_to_contacts": "연락처로 돌아가기",
|
"back_to_contacts": "연락처로 돌아가기",
|
||||||
|
"back_to_email": "이메일로 돌아가기",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "전체",
|
"all": "전체",
|
||||||
"groups": "그룹"
|
"groups": "그룹"
|
||||||
|
|||||||
@@ -1946,6 +1946,7 @@
|
|||||||
"delete_confirm": "Vai tiešām vēlaties dzēst šo kontaktu?",
|
"delete_confirm": "Vai tiešām vēlaties dzēst šo kontaktu?",
|
||||||
"local_mode": "Kontakti tiek glabāti lokāli (serveris neatbalsta JMAP Contacts)",
|
"local_mode": "Kontakti tiek glabāti lokāli (serveris neatbalsta JMAP Contacts)",
|
||||||
"back_to_contacts": "Atpakaļ pie kontaktiem",
|
"back_to_contacts": "Atpakaļ pie kontaktiem",
|
||||||
|
"back_to_email": "Atpakaļ pie e-pasta",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Visi",
|
"all": "Visi",
|
||||||
"groups": "Grupas"
|
"groups": "Grupas"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Weet u zeker dat u dit contact wilt verwijderen?",
|
"delete_confirm": "Weet u zeker dat u dit contact wilt verwijderen?",
|
||||||
"local_mode": "Contacten worden lokaal opgeslagen (server ondersteunt geen JMAP Contacts)",
|
"local_mode": "Contacten worden lokaal opgeslagen (server ondersteunt geen JMAP Contacts)",
|
||||||
"back_to_contacts": "Terug naar contacten",
|
"back_to_contacts": "Terug naar contacten",
|
||||||
|
"back_to_email": "Terug naar e-mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"groups": "Groepen"
|
"groups": "Groepen"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Czy na pewno chcesz usunąć ten kontakt?",
|
"delete_confirm": "Czy na pewno chcesz usunąć ten kontakt?",
|
||||||
"local_mode": "Kontakty są przechowywane lokalnie (serwer nie obsługuje JMAP Contacts)",
|
"local_mode": "Kontakty są przechowywane lokalnie (serwer nie obsługuje JMAP Contacts)",
|
||||||
"back_to_contacts": "Powrót do kontaktów",
|
"back_to_contacts": "Powrót do kontaktów",
|
||||||
|
"back_to_email": "Powrót do wiadomości",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Wszystkie",
|
"all": "Wszystkie",
|
||||||
"groups": "Grupy"
|
"groups": "Grupy"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Tem certeza de que deseja excluir este contato?",
|
"delete_confirm": "Tem certeza de que deseja excluir este contato?",
|
||||||
"local_mode": "Os contatos são armazenados localmente (o servidor não suporta JMAP Contacts)",
|
"local_mode": "Os contatos são armazenados localmente (o servidor não suporta JMAP Contacts)",
|
||||||
"back_to_contacts": "Voltar aos contatos",
|
"back_to_contacts": "Voltar aos contatos",
|
||||||
|
"back_to_email": "Voltar ao e-mail",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Todos",
|
"all": "Todos",
|
||||||
"groups": "Grupos"
|
"groups": "Grupos"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Вы уверены, что хотите удалить этот контакт?",
|
"delete_confirm": "Вы уверены, что хотите удалить этот контакт?",
|
||||||
"local_mode": "Контакты хранятся локально (сервер не поддерживает JMAP Contacts)",
|
"local_mode": "Контакты хранятся локально (сервер не поддерживает JMAP Contacts)",
|
||||||
"back_to_contacts": "Вернуться к контактам",
|
"back_to_contacts": "Вернуться к контактам",
|
||||||
|
"back_to_email": "Вернуться к письму",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Все",
|
"all": "Все",
|
||||||
"groups": "Группы"
|
"groups": "Группы"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Bu kişiyi silmek istediğinizden emin misiniz?",
|
"delete_confirm": "Bu kişiyi silmek istediğinizden emin misiniz?",
|
||||||
"local_mode": "Kişiler yerel olarak saklanıyor (sunucu JMAP Kişilerini desteklemiyor)",
|
"local_mode": "Kişiler yerel olarak saklanıyor (sunucu JMAP Kişilerini desteklemiyor)",
|
||||||
"back_to_contacts": "Kişilere geri dön",
|
"back_to_contacts": "Kişilere geri dön",
|
||||||
|
"back_to_email": "E-postaya geri dön",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "Tümü",
|
"all": "Tümü",
|
||||||
"groups": "Gruplar"
|
"groups": "Gruplar"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "Ви впевнені, що хочете видалити цей контакт?",
|
"delete_confirm": "Ви впевнені, що хочете видалити цей контакт?",
|
||||||
"local_mode": "Контакти зберігаються локально (сервер не підтримує контакти JMAP)",
|
"local_mode": "Контакти зберігаються локально (сервер не підтримує контакти JMAP)",
|
||||||
"back_to_contacts": "Назад до контактів",
|
"back_to_contacts": "Назад до контактів",
|
||||||
|
"back_to_email": "Назад до листа",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "все",
|
"all": "все",
|
||||||
"groups": "Групи"
|
"groups": "Групи"
|
||||||
|
|||||||
@@ -1950,6 +1950,7 @@
|
|||||||
"delete_confirm": "您确定要删除此联系人吗?",
|
"delete_confirm": "您确定要删除此联系人吗?",
|
||||||
"local_mode": "联系人存储在本地(服务器不支持 JMAP 联系人)",
|
"local_mode": "联系人存储在本地(服务器不支持 JMAP 联系人)",
|
||||||
"back_to_contacts": "返回联系人",
|
"back_to_contacts": "返回联系人",
|
||||||
|
"back_to_email": "返回邮件",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"groups": "群组"
|
"groups": "群组"
|
||||||
|
|||||||
Reference in New Issue
Block a user