From 9be6c6bb30cc3a4e9f6d2dd14b53fbba9b6a828e Mon Sep 17 00:00:00 2001 From: beantaco <64012487+beantaco@users.noreply.github.com> Date: Sun, 16 May 2021 23:48:00 +0000 Subject: [PATCH 1/2] More Japanese translations (#1632) * Translate some untranslated strings into Japanese * Tweak some Japanese translations * Add new Japanese translations --- _locales/ja/messages.json | 165 ++++++++++++++++++++++++++++++++++---- 1 file changed, 151 insertions(+), 14 deletions(-) diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 9cdabc033..a7cb90f44 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -1,10 +1,10 @@ { "copyErrorAndQuit": { - "message": "Copy error and quit", + "message": "エラーの文章をコピーして終了", "description": "Shown in the top-level error popup, allowing user to copy the error text and close the app" }, "databaseError": { - "message": "Database Error", + "message": "データベースエラー", "description": "Shown in a popup if the database cannot start up properly" }, "mainMenuFile": { @@ -270,7 +270,7 @@ "description": "Shown in toast when user attempts to send .exe file, for example" }, "stagedPreviewThumbnail": { - "message": "Draft thumbnail link preview for $domain$", + "message": "$domain$ のサムネイルリンクプレビュー(下書き)", "description": "Shown while Session Desktop is fetching metadata for a url in composition area", "placeholders": { "path": { @@ -280,7 +280,7 @@ } }, "previewThumbnail": { - "message": "Thumbnail link preview for $domain$", + "message": "$domain$ のサムネイルリンクプレビュー", "description": "Shown while Session Desktop is fetching metadata for a url in composition area", "placeholders": { "path": { @@ -290,7 +290,7 @@ } }, "stagedImageAttachment": { - "message": "Draft image attachment: $path$", + "message": "添付画像(下書き): $path$", "description": "Alt text for staged attachments", "placeholders": { "path": { @@ -300,15 +300,15 @@ } }, "oneNonImageAtATimeToast": { - "message": "When including a non-image attachment, the limit is one attachment per message.", + "message": "画像でないファイルを添付する場合、メッセージに添付できるファイルは1つのみです。", "description": "An error popup when the user has attempted to add an attachment" }, "cannotMixImageAndNonImageAttachments": { - "message": "You cannot mix non-image and image attachments in one message.", + "message": "画像ファイルと画像でないファイルを合わせてメッセージに添付できません。", "description": "An error popup when the user has attempted to add an attachment" }, "maximumAttachments": { - "message": "You cannot add any more attachments to this message.", + "message": "メッセージに添付できるファイルの数の上限に達しています。", "description": "An error popup when the user has attempted to add an attachment" }, "fileSizeWarning": { @@ -446,7 +446,7 @@ "description": "Shown in a quotation of a message containing a photo if no text was originally provided with that image" }, "cannotUpdate": { - "message": "Cannot Update", + "message": "更新できませんでした", "description": "Shown as the title of our update error dialogs on windows" }, "ok": { @@ -466,7 +466,7 @@ "description": "" }, "deleteWarning": { - "message": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", + "message": "「削除」を選択したら自分の端末からのみメッセージが永久削除されます。よろしいですか?", "description": "" }, "deleteThisMessage": { @@ -562,7 +562,7 @@ "description": "Explain the purpose of the notification settings" }, "disableNotifications": { - "message": "通知をミュート", + "message": "通知を無効にする", "description": "Label for disabling notifications" }, "nameAndMessage": { @@ -985,7 +985,7 @@ "message": "削除" }, "invalidSessionId": { - "message": "Session ID が不正です" + "message": "Session ID が正しくありません" }, "emptyGroupNameError": { "message": "グループ名を入力してください" @@ -1024,7 +1024,7 @@ "message": "アカウントを復元する" }, "newSession": { - "message": "新しい Session" + "message": "新しいセッション" }, "searchFor...": { "message": "会話やメッセージ、連絡先を検索します。" @@ -1088,5 +1088,142 @@ }, "noBlockedContacts": { "message": "ブロックしている連絡先はありません" + }, + "add": { + "message": "追加", + "androidKey": "fragment_add_public_chat_add_button_title_1" + }, + "addContact": { + "message": "連絡先を追加する" + }, + "autoUpdateSettingTitle": { + "message": "自動更新" + }, + "autoUpdateSettingDescription": { + "message": "起動時に自動的に更新の有無を確認する" + }, + "ByUsingThisService...": { + "comment": "By the way the terms of use and privacy policy are presented and the implied consent, there is a risk the terms of use and privacy policy may not legally apply to users in Japan. https://topcourt-law.com/terms_of_service/how-to-get-consent", + "message": "本サービスを利用する場合、利用規約およびプライバシーポリシーに同意するものとします" + }, + "copySessionID": { + "message": "Session ID をコピー", + "description": "Copy to clipboard session ID", + "androidKey": "activity_conversation_menu_copy_session_id" + }, + "createAccount": { + "message": "アカウントを作成" + }, + "deleted": { + "message": "メッセージが削除されました", + "description": "Toast validation when a single or several messages were deleted" + }, + "deleteForEveryone": { + "message": "全員から削除", + "description": "Menu item for deleting messages, title case." + }, + "deleteMessageForEveryone": { + "message": "全員からメッセージを削除", + "description": "Menu item for deleting messages, title case." + }, + "deleteMessagesForEveryone": { + "comment": "Same content as in \"deleteMessageForEveryone\".", + "message": "全員からメッセージを削除", + "description": "Menu item for deleting messages, title case." + }, + "deleteMultiplePublicWarning": { + "comment": "Same content as in \"deletePublicWarning\".", + "message": "「削除」を選択したら公開グループからメッセージが永久削除されます。よろしいですか?" + }, + "deleteMultipleWarning": { + "comment": "Same content as in \"deleteWarning\".", + "message": "「削除」を選択したら自分の端末からのみメッセージが永久削除されます。よろしいですか?" + }, + "deletePublicConversationConfirmation": { + "message": "「削除」を選択したら自分の端末からのみ公開グループのメッセージが永久削除されます。よろしいですか?", + "description": "Confirmation dialog text that asks the user if they really wish to delete the open group messages locally. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone." + }, + "deletePublicWarning": { + "comment": "Does deletion apply to just the open group or does it extend to all members' devices?", + "message": "「削除」を選択したら公開グループからメッセージが永久削除されます。よろしいですか?" + }, + "editProfileModalTitle": { + "message": "プロフィール", + "description": "Title for the Edit Profile modal" + }, + "enterOptionalPassword": { + "message": "パスワード入力(任意)" + }, + "hideMenuBarDescription": { + "message": "メニューバーの表示を切り替える", + "description": "Label text for menu bar visibility setting" + }, + "hideMenuBarTitle": { + "message": "メニューバーを隠す", + "description": "Label text for menu bar visibility setting" + }, + "mediaPermissionsTitle": { + "message": "マイクとカメラ" + }, + "password": { + "message": "パスワード", + "description": "Placeholder for password input" + }, + "passwordCharacterError": { + "message": "パスワードには英数字と記号の文字しか使えません", + "description": "Error string shown to the user when password contains an invalid character" + }, + "passwordLengthError": { + "message": "パスワードの長さを6文字から64文字にしてください", + "description": "Error string shown to the user when password doesn't meet length criteria" + }, + "passwordsDoNotMatch": { + "message": "パスワードが一致しません" + }, + "passwordTypeError": { + "message": "パスワードは文字列でなければいけません", + "description": "Error string shown to the user when password is not a string" + }, + "passwordViewTitle": { + "message": "パスワード入力", + "description": "The title shown when user needs to type in a password to unlock the messenger" + }, + "readReceiptSettingDescription": { + "message": "メッセージが読まれた状態の表示と送信をする(すべてのセッションに既読通知を有功にする)。", + "description": "Description of the read receipts setting" + }, + "replyingToMessage": { + "message": "このメッセージへの返信:" + }, + "selectMessage": { + "message": "メッセージを選択", + "description": "Button action that the user can click to select the message" + }, + "setAccountPasswordDescription": { + "message": "Session のスクリーンのロック解除にパスワードを要求する。スクリーンロック中にもメッセージの通知が受信できます。Session の通知設定でメッセージの通知に表示される情報を調整できます。", + "description": "Description for set account password setting view" + }, + "setAccountPasswordTitle": { + "message": "アカウントのパスワード設定", + "description": "Prompt for user to set account password in settings view" + }, + "signIn": { + "message": "ログイン" + }, + "spellCheckTitle": { + "message": "スペルチェック", + "description": "Description of the media permission description" + }, + "typingIndicatorsSettingDescription": { + "message": "メッセージが入力中である状態の表示と送信をする(すべてのセッションに適用される)。", + "description": "Description of the typing indicators setting" + }, + "unpairDeviceWarning": { + "message": "この端末をリンク解除してもよろしいですか?", + "description": "Warning for device unlinking in settings view" + }, + "zoomFactorSettingTitle": { + "message": "ズーム", + "description": "Title of the Zoom Factor setting" } -} \ No newline at end of file +} From 3854d0e10d984677a0f030e2dfe83864ef0f1a61 Mon Sep 17 00:00:00 2001 From: Warrick Date: Tue, 18 May 2021 13:12:51 +1000 Subject: [PATCH 2/2] WIP: Closed group reliability (#1630) * WIP: added non-durable messaging function. * WIP: Non-durable sending * WIP: adding dialog box. * Creating dialog if group invite message promises don't return true. * removed console log * applied PR changes, linting and formatting. * WIP: allowing resend invite to failures. * using lookup. * WIP: recursively opening dialog. * WIP: debugging reject triggering on confirmation modal. * register events fix. * Closed group invite retry dialog working. * Added english text to messages. * Prevent saving of hexkey pair if it already exists. * Fixed nickname edit input trimming end letter. * Don't show closed group invite dialog unless it has failed at least once. * Fix linting error. * Fix plurality. * Ensure admin members are included in all invite reattempts, mixed plurality. --- _locales/en/messages.json | 32 +++++ js/views/session_confirm_view.js | 13 +- .../session/SessionNicknameDialog.tsx | 2 +- ts/receiver/closedGroups.ts | 133 ++++++++++++++++-- ts/session/sending/MessageQueue.ts | 25 +++- 5 files changed, 185 insertions(+), 20 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 39e629741..4f4796c53 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1387,6 +1387,38 @@ "description": "Button action that the user can click to edit a group name (open)", "androidKey": "conversation__menu_edit_group" }, + "closedGroupInviteFailTitle": { + "message": "Group Invitation Failed", + "description": "Title for the dialog of a failed group invite modal" + }, + "closedGroupInviteFailTitlePlural": { + "message": "Group Invitations Failed", + "description": "Title for the dialog of a failed group invite modal plural" + }, + "closedGroupInviteFailMessage": { + "message": "Unable to successfully invite a group member", + "description": "Message for the dialog of a failed group invite modal" + }, + "closedGroupInviteFailMessagePlural": { + "message": "Unable to successfully invite all group members", + "description": "Message for the dialog of a failed group invite modal plural" + }, + "closedGroupInviteOkText": { + "message": "Retry invitations", + "description": "Text for the OK button of a closed group invite failure" + }, + "closedGroupInviteSuccessTitlePlural": { + "message": "Group Invitations Completed", + "description": "The title for the modal dialog when a closed group invite retry succeeds" + }, + "closedGroupInviteSuccessTitle": { + "message": "Group Invitation Succeeded", + "description": "The title for the modal dialog when a closed group invite retry succeeds" + }, + "closedGroupInviteSuccessMessage": { + "message": "Successfully invited closed group members", + "description": "The message for the modal dialog when a closed group invite retry succeeds" + }, "editGroupName": { "message": "Edit group name", "description": "Button action that the user can click to edit a group name (closed)" diff --git a/js/views/session_confirm_view.js b/js/views/session_confirm_view.js index 1d5a6b8a8..8d29a14d2 100644 --- a/js/views/session_confirm_view.js +++ b/js/views/session_confirm_view.js @@ -33,32 +33,39 @@ unregisterEvents() { document.removeEventListener('keyup', this.props.onClickClose, false); + if (this.confirmView && this.confirmView.el) { + window.ReactDOM.unmountComponentAtNode(this.confirmView.el); + } + this.$('.session-confirm-wrapper').remove(); }, render() { this.$('.session-confirm-wrapper').remove(); + this.registerEvents(); this.confirmView = new Whisper.ReactWrapperView({ className: 'loki-dialog modal session-confirm-wrapper', Component: window.Signal.Components.SessionConfirm, props: this.props, }); - this.registerEvents(); this.$el.prepend(this.confirmView.el); }, ok() { - this.$('.session-confirm-wrapper').remove(); this.unregisterEvents(); + + this.$('.session-confirm-wrapper').remove(); if (this.props.resolve) { this.props.resolve(); } }, cancel() { - this.$('.session-confirm-wrapper').remove(); this.unregisterEvents(); + + this.$('.session-confirm-wrapper').remove(); + if (this.props.reject) { this.props.reject(); } diff --git a/ts/components/session/SessionNicknameDialog.tsx b/ts/components/session/SessionNicknameDialog.tsx index aaf1882e3..d68143f6e 100644 --- a/ts/components/session/SessionNicknameDialog.tsx +++ b/ts/components/session/SessionNicknameDialog.tsx @@ -59,7 +59,7 @@ const SessionNicknameInner = (props: Props) => { type="nickname" id="nickname-modal-input" placeholder={window.i18n('nicknamePlaceholder')} - onKeyPress={e => { + onKeyUp={e => { void onNicknameInput(_.cloneDeep(e)); }} /> diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 7c89425a4..6f9fd0039 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -34,6 +34,7 @@ import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgo import { queueAllCachedFromSource } from './receiver'; import { actions as conversationActions } from '../state/ducks/conversations'; import { SwarmPolling } from '../session/snode_api/swarmPolling'; +import { MessageModel } from '../models/message'; export const distributingClosedGroupEncryptionKeyPairs = new Map(); @@ -885,7 +886,122 @@ export async function createClosedGroup(groupName: string, members: Array { + const allInvitesSent = await sendToGroupMembers( + listOfMembers, + groupPublicKey, + groupName, + admins, + encryptionKeyPair, + dbMessage + ); + + if (allInvitesSent) { + const newHexKeypair = encryptionKeyPair.toHexKeyPair(); + + const isHexKeyPairSaved = await isKeyPairAlreadySaved(groupPublicKey, newHexKeypair); + + if (!isHexKeyPairSaved) { + // tslint:disable-next-line: no-non-null-assertion + await addClosedGroupEncryptionKeyPair(groupPublicKey, encryptionKeyPair.toHexKeyPair()); + } else { + window.log.info('Dropping already saved keypair for group', groupPublicKey); + } + + // Subscribe to this group id + SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey)); + } + + await forceSyncConfigurationNowIfNeeded(); + + window.inboxStore?.dispatch(conversationActions.openConversationExternal(groupPublicKey)); +} + +/** + * Sends a group invite message to each member of the group. + * @returns Array of promises for group invite messages sent to group members + */ +async function sendToGroupMembers( + listOfMembers: Array, + groupPublicKey: string, + groupName: string, + admins: Array, + encryptionKeyPair: ECKeyPair, + dbMessage: MessageModel, + isRetry: boolean = false +): Promise { + const promises = createInvitePromises( + listOfMembers, + groupPublicKey, + groupName, + admins, + encryptionKeyPair, + dbMessage + ); + window.log.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`); + // evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog + const inviteResults = await Promise.all(promises); + const allInvitesSent = _.every(inviteResults, Boolean); + + if (allInvitesSent) { + if (isRetry) { + const invitesTitle = + inviteResults.length > 1 + ? window.i18n('closedGroupInviteSuccessTitlePlural') + : window.i18n('closedGroupInviteSuccessTitle'); + window.confirmationDialog({ + title: invitesTitle, + message: window.i18n('closedGroupInviteSuccessMessage'), + }); + } + return allInvitesSent; + } else { + // Confirmation dialog that recursively calls sendToGroupMembers on resolve + window.confirmationDialog({ + title: + inviteResults.length > 1 + ? window.i18n('closedGroupInviteFailTitlePlural') + : window.i18n('closedGroupInviteFailTitle'), + message: + inviteResults.length > 1 + ? window.i18n('closedGroupInviteFailMessagePlural') + : window.i18n('closedGroupInviteFailMessage'), + okText: window.i18n('closedGroupInviteOkText'), + resolve: async () => { + const membersToResend: Array = new Array(); + inviteResults.forEach((result, index) => { + const member = listOfMembers[index]; + // group invite must always contain the admin member. + if (result !== true || admins.includes(member)) { + membersToResend.push(member); + } + }); + if (membersToResend.length > 0) { + const isRetrySend = true; + await sendToGroupMembers( + membersToResend, + groupPublicKey, + groupName, + admins, + encryptionKeyPair, + dbMessage, + isRetrySend + ); + } + }, + }); + } + return allInvitesSent; +} + +function createInvitePromises( + listOfMembers: Array, + groupPublicKey: string, + groupName: string, + admins: Array, + encryptionKeyPair: ECKeyPair, + dbMessage: MessageModel +) { + return listOfMembers.map(async m => { const messageParams: ClosedGroupNewMessageParams = { groupId: groupPublicKey, name: groupName, @@ -897,19 +1013,6 @@ export async function createClosedGroup(groupName: string, members: Array { + let rawMessage; + try { + rawMessage = await MessageUtils.toRawMessage(user, message); + const wrappedEnvelope = await MessageSender.send(rawMessage); + await MessageSentHandler.handleMessageSentSuccess(rawMessage, wrappedEnvelope); + return !!wrappedEnvelope; + } catch (error) { + if (rawMessage) { + await MessageSentHandler.handleMessageSentFailure(rawMessage, error); + } + return false; + } + } + public async processPending(device: PubKey) { const messages = await this.pendingMessageCache.getForDevice(device);