Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,10 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
PODFILE_DIR = "$(SRCROOT)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -862,7 +865,10 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
PODFILE_DIR = "$(SRCROOT)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
let Clipboard: { setString: (string: string) => void } | undefined;
type SetClipboardStringOptions = {
onFailure?: (error: unknown) => void;
onSuccess?: () => void;
};

let Clipboard:
| {
setString?: (text: string) => void;
setStringAsync?: (text: string) => Promise<boolean>;
}
| undefined;

try {
Clipboard = require('expo-clipboard');
Expand All @@ -12,6 +22,35 @@ if (!Clipboard) {
);
}

export const setClipboardString = Clipboard
? (string: string) => Clipboard?.setString(string)
: null;
export const setClipboardString =
Clipboard && (Clipboard.setStringAsync || Clipboard.setString)
? (text: string, options?: SetClipboardStringOptions) => {
try {
// `expo-clipboard` removed the synchronous `setString` in favour of
// the async API. Keep this handler synchronous (void return,
// like the legacy `setString`) and fire the async write without
// awaiting, reporting the outcome through the callbacks. Fall back to
// `setString` for older versions.
if (Clipboard?.setStringAsync) {
Clipboard.setStringAsync(text)
.then((success) => {
if (success) {
options?.onSuccess?.();
} else {
options?.onFailure?.(new Error('Copying to clipboard failed'));
}
})
.catch((error: unknown) => {
console.log('Copying to clipboard failed...', error);
options?.onFailure?.(error);
});
} else {
Clipboard?.setString?.(text);
options?.onSuccess?.();
}
} catch (error) {
console.log('Copying to clipboard failed...', error);
options?.onFailure?.(error);
}
}
: null;
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
let Clipboard: { setString: (string: string) => void } | undefined;
type SetClipboardStringOptions = {
onFailure?: (error: unknown) => void;
onSuccess?: () => void;
};

let Clipboard: { setString?: (text: string) => void } | undefined;

try {
Clipboard = require('@react-native-clipboard/clipboard').default;
Expand All @@ -7,6 +12,14 @@ try {
console.log('@react-native-clipboard/clipboard is not installed');
}

export const setClipboardString = Clipboard
? (string: string) => (Clipboard ? Clipboard.setString(string) : {})
export const setClipboardString = Clipboard?.setString
? (text: string, options?: SetClipboardStringOptions) => {
try {
Clipboard?.setString?.(text);
options?.onSuccess?.();
} catch (error) {
console.log('Copying to clipboard failed...', error);
options?.onFailure?.(error);
}
}
: null;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Channel, LocalMessage, StreamChat } from 'stream-chat';

import { ChannelProvider } from '../../../../contexts/channelContext/ChannelContext';
import { ChatProvider } from '../../../../contexts/chatContext/ChatContext';
import { NativeHandlers } from '../../../../native';
import { NotificationTargetProvider } from '../../../Notifications/NotificationTargetContext';
import { useMessageActionHandlers } from '../useMessageActionHandlers';

Expand Down Expand Up @@ -129,3 +130,73 @@ describe('useMessageActionHandlers notifications', () => {
});
});
});

describe('useMessageActionHandlers copy', () => {
const originalSetClipboardString = NativeHandlers.setClipboardString;

afterEach(() => {
NativeHandlers.setClipboardString = originalSetClipboardString;
});

it('notifies when copying a message succeeds', () => {
NativeHandlers.setClipboardString = jest.fn((_text, options) => options?.onSuccess?.());
const client = createClient();
const message = createMessage();
const { result } = renderUseMessageActionHandlers({ client, message });

act(() => {
result.current.handleCopyMessage();
});

expect(NativeHandlers.setClipboardString).toHaveBeenCalledWith(
'Message text',
expect.objectContaining({ onFailure: expect.any(Function), onSuccess: expect.any(Function) }),
);
expect(client.notifications.add).toHaveBeenCalledWith({
message: 'Message copied to clipboard',
options: {
severity: 'success',
tags: ['target:channel:channel:messaging:general'],
type: 'clipboard:message:copy:success',
},
origin: { context: { message }, emitter: 'MessageActions' },
});
});

it('notifies when copying a message fails', () => {
const error = new Error('Clipboard unavailable');
NativeHandlers.setClipboardString = jest.fn((_text, options) => options?.onFailure?.(error));
const client = createClient();
const message = createMessage();
const { result } = renderUseMessageActionHandlers({ client, message });

act(() => {
result.current.handleCopyMessage();
});

expect(client.notifications.add).toHaveBeenCalledWith({
message: 'Failed to copy message',
options: {
originalError: error,
severity: 'error',
tags: ['target:channel:channel:messaging:general'],
type: 'clipboard:message:copy:failed',
},
origin: { context: { message }, emitter: 'MessageActions' },
});
});

it('does not copy or notify when the message has no text', () => {
NativeHandlers.setClipboardString = jest.fn();
const client = createClient();
const message = createMessage({ text: '' });
const { result } = renderUseMessageActionHandlers({ client, message });

act(() => {
result.current.handleCopyMessage();
});

expect(NativeHandlers.setClipboardString).not.toHaveBeenCalled();
expect(client.notifications.add).not.toHaveBeenCalled();
});
});
21 changes: 20 additions & 1 deletion package/src/components/Message/hooks/useMessageActionHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,26 @@ export const useMessageActionHandlers = ({
if (!message.text) {
return;
}
NativeHandlers.setClipboardString(translatedMessage?.text ?? message.text);
NativeHandlers.setClipboardString(translatedMessage?.text ?? message.text, {
onFailure: (error) => {
addNotification({
message: t('Failed to copy message'),
options: {
...getNotificationErrorOptions(error),
severity: 'error',
type: 'clipboard:message:copy:failed',
},
origin: { context: { message }, emitter: 'MessageActions' },
});
},
onSuccess: () => {
addNotification({
message: t('Message copied to clipboard'),
options: { severity: 'success', type: 'clipboard:message:copy:success' },
origin: { context: { message }, emitter: 'MessageActions' },
});
},
});
});

const handleDeleteMessage = useStableCallback(() => {
Expand Down
4 changes: 3 additions & 1 deletion package/src/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "ستتوقف عن تلقي الرسائل من {{ name }}. يمكنك الانضمام مجددًا في أي وقت.",
"group": "المجموعة",
"No members found": "لم يتم العثور على أعضاء",
"a11y/Search members": "البحث عن الأعضاء"
"a11y/Search members": "البحث عن الأعضاء",
"Message copied to clipboard": "تم نسخ الرسالة إلى الحافظة",
"Failed to copy message": "فشل نسخ الرسالة"
}
2 changes: 2 additions & 0 deletions package/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"Mark as Unread": "Mark as Unread",
"Maximum number of files reached": "Maximum number of files reached",
"Message Reactions": "Message Reactions",
"Message copied to clipboard": "Message copied to clipboard",
"Message deleted": "Message deleted",
"Message has been successfully flagged": "Message has been successfully flagged",
"Message flagged": "Message flagged",
Expand Down Expand Up @@ -393,6 +394,7 @@
"Command not available while replying": "Command not available while replying",
"Error reproducing the recording": "Error reproducing the recording",
"Error uploading attachment": "Error uploading attachment",
"Failed to copy message": "Failed to copy message",
"Failed to create the poll": "Failed to create the poll",
"Failed to create the poll due to {{reason}}": "Failed to create the poll due to {{reason}}",
"Failed to end the poll": "Failed to end the poll",
Expand Down
4 changes: 3 additions & 1 deletion package/src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Dejarás de recibir mensajes de {{ name }}. Puedes volver a unirte cuando quieras.",
"group": "el grupo",
"No members found": "No se encontraron miembros",
"a11y/Search members": "Buscar miembros"
"a11y/Search members": "Buscar miembros",
"Message copied to clipboard": "Mensaje copiado al portapapeles",
"Failed to copy message": "No se pudo copiar el mensaje"
}
4 changes: 3 additions & 1 deletion package/src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Vous ne recevrez plus de messages de {{ name }}. Vous pouvez rejoindre à tout moment.",
"group": "ce groupe",
"No members found": "Aucun membre trouvé",
"a11y/Search members": "Rechercher des membres"
"a11y/Search members": "Rechercher des membres",
"Message copied to clipboard": "Message copié dans le presse-papiers",
"Failed to copy message": "Échec de la copie du message"
}
4 changes: 3 additions & 1 deletion package/src/i18n/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "תפסיק/תפסיקי לקבל הודעות מ-{{ name }}. ניתן להצטרף בחזרה בכל עת.",
"group": "הקבוצה",
"No members found": "לא נמצאו חברים",
"a11y/Search members": "חיפוש חברים"
"a11y/Search members": "חיפוש חברים",
"Message copied to clipboard": "ההודעה הועתקה ללוח",
"Failed to copy message": "העתקת ההודעה נכשלה"
}
4 changes: 3 additions & 1 deletion package/src/i18n/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "आपको {{ name }} से संदेश मिलना बंद हो जाएंगे। आप कभी भी दोबारा जुड़ सकते हैं।",
"group": "ग्रुप",
"No members found": "कोई सदस्य नहीं मिला",
"a11y/Search members": "सदस्य खोजें"
"a11y/Search members": "सदस्य खोजें",
"Message copied to clipboard": "संदेश क्लिपबोर्ड पर कॉपी किया गया",
"Failed to copy message": "संदेश कॉपी करने में विफल रहा"
}
4 changes: 3 additions & 1 deletion package/src/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Non riceverai più messaggi da {{ name }}. Puoi rientrare in qualsiasi momento.",
"group": "il gruppo",
"No members found": "Nessun membro trovato",
"a11y/Search members": "Cerca membri"
"a11y/Search members": "Cerca membri",
"Message copied to clipboard": "Messaggio copiato negli appunti",
"Failed to copy message": "Impossibile copiare il messaggio"
}
4 changes: 3 additions & 1 deletion package/src/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "{{ name }} からのメッセージが届かなくなります。いつでも再参加できます。",
"group": "グループ",
"No members found": "メンバーが見つかりません",
"a11y/Search members": "メンバーを検索"
"a11y/Search members": "メンバーを検索",
"Message copied to clipboard": "メッセージをクリップボードにコピーしました",
"Failed to copy message": "メッセージのコピーに失敗しました"
}
4 changes: 3 additions & 1 deletion package/src/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "{{ name }}에서 더 이상 메시지를 받지 않게 됩니다. 언제든지 다시 참여할 수 있습니다.",
"group": "그룹",
"No members found": "멤버를 찾을 수 없습니다",
"a11y/Search members": "멤버 검색"
"a11y/Search members": "멤버 검색",
"Message copied to clipboard": "메시지가 클립보드에 복사되었습니다",
"Failed to copy message": "메시지 복사에 실패했습니다"
}
4 changes: 3 additions & 1 deletion package/src/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Je ontvangt geen berichten meer van {{ name }}. Je kunt op elk moment opnieuw deelnemen.",
"group": "de groep",
"No members found": "Geen leden gevonden",
"a11y/Search members": "Zoek leden"
"a11y/Search members": "Zoek leden",
"Message copied to clipboard": "Bericht gekopieerd naar klembord",
"Failed to copy message": "Kopiëren van bericht mislukt"
}
4 changes: 3 additions & 1 deletion package/src/i18n/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Você deixará de receber mensagens de {{ name }}. Pode entrar novamente a qualquer momento.",
"group": "o grupo",
"No members found": "Nenhum membro encontrado",
"a11y/Search members": "Pesquisar membros"
"a11y/Search members": "Pesquisar membros",
"Message copied to clipboard": "Mensagem copiada para a área de transferência",
"Failed to copy message": "Falha ao copiar a mensagem"
}
4 changes: 3 additions & 1 deletion package/src/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Вы перестанете получать сообщения от {{ name }}. Вы можете присоединиться снова в любое время.",
"group": "группы",
"No members found": "Участники не найдены",
"a11y/Search members": "Поиск участников"
"a11y/Search members": "Поиск участников",
"Message copied to clipboard": "Сообщение скопировано в буфер обмена",
"Failed to copy message": "Не удалось скопировать сообщение"
}
4 changes: 3 additions & 1 deletion package/src/i18n/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,7 @@
"You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "{{ name }} kanalından mesaj almayı bırakacaksınız. İstediğiniz zaman tekrar katılabilirsiniz.",
"group": "grup",
"No members found": "Üye bulunamadı",
"a11y/Search members": "Üye ara"
"a11y/Search members": "Üye ara",
"Message copied to clipboard": "Mesaj panoya kopyalandı",
"Failed to copy message": "Mesaj kopyalanamadı"
}
11 changes: 10 additions & 1 deletion package/src/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,16 @@ type SaveFileOptions = {
};
type SaveFile = (options: SaveFileOptions) => Promise<string> | never;

type SetClipboardString = (text: string) => Promise<void> | never;
type SetClipboardStringOptions = {
/** Invoked when writing to the clipboard fails. */
onFailure?: (error: unknown) => void;
/** Invoked after the text has been handed to the clipboard successfully. */
onSuccess?: () => void;
};
// TODO(next-major): make this handler async and return the write outcome directly.
// It's kept synchronous (void) for backwards compatibility, so success/failure are
// reported via the `onSuccess`/`onFailure` callbacks instead of a Promise.
type SetClipboardString = (text: string, options?: SetClipboardStringOptions) => void;

type ShareOptions = {
type?: string;
Expand Down