mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge branch 'clearnet' into scoring-system
This commit is contained in:
commit
c2298c4c30
16 changed files with 469 additions and 83 deletions
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
@ -5,9 +5,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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": "本サービスを利用する場合、<a href=\"https://getsession.org/legal/#tos\">利用規約</a>および<a href=\"https://getsession.org/privacy-policy/\" target=\"_blank\">プライバシーポリシー</a>に同意するものとします"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,15 +383,8 @@
|
|||
};
|
||||
|
||||
window.showNicknameDialog = params => {
|
||||
const options = {
|
||||
title: params.title || undefined,
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
convoId: params.convoId || undefined,
|
||||
};
|
||||
|
||||
if (appView) {
|
||||
appView.showNicknameDialog(options);
|
||||
appView.showNicknameDialog(params);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "session-desktop",
|
||||
"productName": "Session",
|
||||
"description": "Private messaging from your desktop",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.2",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Loki Project",
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ConversationController } from '../../session/conversations/ConversationController';
|
||||
import { SessionModal } from './SessionModal';
|
||||
import { SessionButton, SessionButtonColor } from './SessionButton';
|
||||
import { SessionButton } from './SessionButton';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
type Props = {
|
||||
message: string;
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
onOk?: any;
|
||||
onClose?: any;
|
||||
onClickOk: any;
|
||||
onClickClose: any;
|
||||
hideCancel: boolean;
|
||||
okTheme: SessionButtonColor;
|
||||
theme: DefaultTheme;
|
||||
convoId?: string;
|
||||
convoId: string;
|
||||
};
|
||||
|
||||
const SessionNicknameInner = (props: Props) => {
|
||||
const { title = '', message, onClickOk, onClickClose, convoId, placeholder } = props;
|
||||
const showHeader = true;
|
||||
const { onClickOk, onClickClose, convoId, theme } = props;
|
||||
const [nickname, setNickname] = useState('');
|
||||
|
||||
/**
|
||||
|
@ -30,9 +24,10 @@ const SessionNicknameInner = (props: Props) => {
|
|||
const onNicknameInput = async (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
await saveNickname();
|
||||
} else {
|
||||
const currentNicknameEntered = event.target.value;
|
||||
setNickname(currentNicknameEntered);
|
||||
}
|
||||
const currentNicknameEntered = event.target.value;
|
||||
setNickname(currentNicknameEntered);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -49,24 +44,24 @@ const SessionNicknameInner = (props: Props) => {
|
|||
|
||||
return (
|
||||
<SessionModal
|
||||
title={title}
|
||||
title={window.i18n('changeNickname')}
|
||||
onClose={onClickClose}
|
||||
showExitIcon={false}
|
||||
showHeader={showHeader}
|
||||
theme={props.theme}
|
||||
showHeader={true}
|
||||
theme={theme}
|
||||
>
|
||||
{!showHeader && <div className="spacer-lg" />}
|
||||
|
||||
<div className="session-modal__centered">
|
||||
<span className="subtle">{message}</span>
|
||||
<span className="subtle">{window.i18n('changeNicknameMessage')}</span>
|
||||
<div className="spacer-lg" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="nickname"
|
||||
id="nickname-modal-input"
|
||||
placeholder={placeholder}
|
||||
onKeyUp={onNicknameInput}
|
||||
placeholder={window.i18n('nicknamePlaceholder')}
|
||||
onKeyUp={e => {
|
||||
void onNicknameInput(_.cloneDeep(e));
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
|
|
|
@ -1312,9 +1312,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
);
|
||||
}
|
||||
window.showNicknameDialog({
|
||||
title: window.i18n('changeNickname') || 'Change Nickname',
|
||||
message: window.i18n('changeNicknameMessage') || '',
|
||||
placeholder: window.i18n('nicknamePlaceholder') || '',
|
||||
convoId: this.id,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<string, ECKeyPair>();
|
||||
|
||||
|
@ -885,7 +886,122 @@ export async function createClosedGroup(groupName: string, members: Array<string
|
|||
convo.updateLastMessage();
|
||||
|
||||
// Send a closed group update message to all members individually
|
||||
const promises = listOfMembers.map(async m => {
|
||||
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<string>,
|
||||
groupPublicKey: string,
|
||||
groupName: string,
|
||||
admins: Array<string>,
|
||||
encryptionKeyPair: ECKeyPair,
|
||||
dbMessage: MessageModel,
|
||||
isRetry: boolean = false
|
||||
): Promise<any> {
|
||||
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<string> = new Array<string>();
|
||||
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<string>,
|
||||
groupPublicKey: string,
|
||||
groupName: string,
|
||||
admins: Array<string>,
|
||||
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<string
|
|||
expireTimer: 0,
|
||||
};
|
||||
const message = new ClosedGroupNewMessage(messageParams);
|
||||
|
||||
return getMessageQueue().sendToPubKey(PubKey.cast(m), message);
|
||||
return getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(m), message);
|
||||
});
|
||||
window.log.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`);
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
await addClosedGroupEncryptionKeyPair(groupPublicKey, encryptionKeyPair.toHexKeyPair());
|
||||
|
||||
// Subscribe to this group id
|
||||
SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
await forceSyncConfigurationNowIfNeeded();
|
||||
|
||||
window.inboxStore?.dispatch(conversationActions.openConversationExternal(groupPublicKey));
|
||||
}
|
||||
|
|
|
@ -12,14 +12,15 @@ const PADDING_BYTE = 0x00;
|
|||
*/
|
||||
export function removeMessagePadding(paddedData: ArrayBuffer): ArrayBuffer {
|
||||
const paddedPlaintext = new Uint8Array(paddedData);
|
||||
window.log.info('Removing message padding...');
|
||||
window?.log.info('Removing message padding...');
|
||||
for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) {
|
||||
if (paddedPlaintext[i] === 0x80) {
|
||||
const plaintext = new Uint8Array(i);
|
||||
plaintext.set(paddedPlaintext.subarray(0, i));
|
||||
return plaintext.buffer;
|
||||
} else if (paddedPlaintext[i] !== PADDING_BYTE) {
|
||||
throw new Error('Invalid padding');
|
||||
window?.log.warn('got a message without padding... Letting it through for now');
|
||||
return paddedPlaintext;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +32,7 @@ export function removeMessagePadding(paddedData: ArrayBuffer): ArrayBuffer {
|
|||
* @param messageBuffer The buffer to add padding to.
|
||||
*/
|
||||
export function addMessagePadding(messageBuffer: Uint8Array): Uint8Array {
|
||||
window.log?.info('Adding message padding...');
|
||||
window?.log?.info('Adding message padding...');
|
||||
|
||||
const plaintext = new Uint8Array(getPaddedMessageLength(messageBuffer.byteLength + 1) - 1);
|
||||
plaintext.set(new Uint8Array(messageBuffer));
|
||||
|
@ -58,25 +59,19 @@ export function getUnpaddedAttachment(
|
|||
data: ArrayBuffer,
|
||||
unpaddedExpectedSize: number
|
||||
): ArrayBuffer | null {
|
||||
window.log?.info('Removing attachment padding...');
|
||||
window?.log?.info('Removing attachment padding...');
|
||||
|
||||
// to have a padding we must have a strictly longer length expected
|
||||
if (data.byteLength <= unpaddedExpectedSize) {
|
||||
return null;
|
||||
}
|
||||
const dataUint = new Uint8Array(data);
|
||||
for (let i = unpaddedExpectedSize; i < data.byteLength; i++) {
|
||||
if (dataUint[i] !== PADDING_BYTE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// we now consider that anything coming after the expected size is padding, no matter what there is there
|
||||
return data.slice(0, unpaddedExpectedSize);
|
||||
}
|
||||
|
||||
export function addAttachmentPadding(data: ArrayBuffer): ArrayBuffer {
|
||||
const originalUInt = new Uint8Array(data);
|
||||
window.log.info('Adding attchment padding...');
|
||||
window?.log.info('Adding attchment padding...');
|
||||
|
||||
const paddedSize = Math.max(
|
||||
541,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PendingMessageCache } from './PendingMessageCache';
|
||||
import { JobQueue, UserUtils } from '../utils';
|
||||
import { JobQueue, MessageUtils, UserUtils } from '../utils';
|
||||
import { PubKey, RawMessage } from '../types';
|
||||
import { MessageSender } from '.';
|
||||
import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage';
|
||||
|
@ -146,6 +146,29 @@ export class MessageQueue {
|
|||
await this.process(PubKey.cast(ourPubKey), message, sentCb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message that awaits until the message is completed sending
|
||||
* @param user user pub key to send to
|
||||
* @param message Message to be sent
|
||||
*/
|
||||
public async sendToPubKeyNonDurably(
|
||||
user: PubKey,
|
||||
message: ClosedGroupNewMessage
|
||||
): Promise<boolean> {
|
||||
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);
|
||||
|
||||
|
|
92
ts/test/session/unit/padding/Padding_test.ts
Normal file
92
ts/test/session/unit/padding/Padding_test.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression
|
||||
|
||||
import chai from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import _ from 'lodash';
|
||||
import { describe } from 'mocha';
|
||||
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import {
|
||||
addAttachmentPadding,
|
||||
addMessagePadding,
|
||||
getUnpaddedAttachment,
|
||||
removeMessagePadding,
|
||||
} from '../../../../session/crypto/BufferPadding';
|
||||
chai.use(chaiAsPromised as any);
|
||||
chai.should();
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
// tslint:disable-next-line: max-func-body-length
|
||||
describe('Padding', () => {
|
||||
describe('Attachment padding', () => {
|
||||
it('add padding', () => {
|
||||
const bufferIn = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
|
||||
const paddedBuffer = addAttachmentPadding(bufferIn);
|
||||
expect(paddedBuffer.byteLength).to.equal(541);
|
||||
expect(new Uint8Array(paddedBuffer.slice(0, bufferIn.length))).to.equalBytes(bufferIn);
|
||||
// this makes sure that the padding is just the 0 bytes
|
||||
expect(new Uint8Array(paddedBuffer.slice(bufferIn.length))).to.equalBytes(
|
||||
new Uint8Array(541 - bufferIn.length)
|
||||
);
|
||||
});
|
||||
|
||||
it('remove padding', () => {
|
||||
// padding can be anything after the expected size
|
||||
const expectedSize = 10;
|
||||
const paddedBuffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 5]);
|
||||
|
||||
const paddingRemoveBuffer = getUnpaddedAttachment(paddedBuffer, expectedSize);
|
||||
|
||||
expect(paddingRemoveBuffer?.byteLength).to.equal(expectedSize);
|
||||
expect(paddingRemoveBuffer).to.equalBytes(paddedBuffer.slice(0, expectedSize));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message padding', () => {
|
||||
it('add padding', () => {
|
||||
const bufferIn = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
|
||||
const paddedMessage = addMessagePadding(bufferIn);
|
||||
expect(paddedMessage.byteLength).to.equal(159);
|
||||
// for message padding, we have [bufferIn, 0x80, 0x00, 0x00, 0x00, ...]
|
||||
expect(new Uint8Array(paddedMessage.slice(0, bufferIn.length))).to.equalBytes(bufferIn);
|
||||
expect(paddedMessage[bufferIn.length]).to.equal(0x80);
|
||||
// this makes sure that the padding is just the 0 bytes
|
||||
expect(new Uint8Array(paddedMessage.slice(bufferIn.length + 1))).to.equalBytes(
|
||||
new Uint8Array(159 - bufferIn.length - 1)
|
||||
);
|
||||
});
|
||||
|
||||
it('remove padding', () => {
|
||||
const expectedSize = 10;
|
||||
const paddedBuffer = new Uint8Array([
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
128,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
|
||||
const unpaddedMessage = removeMessagePadding(paddedBuffer);
|
||||
// for message padding, we have [paddedBuffer, 0x80, 0x00, 0x00, 0x00, ...]
|
||||
expect(unpaddedMessage?.byteLength).to.equal(expectedSize);
|
||||
expect(new Uint8Array(unpaddedMessage)).to.equalBytes(paddedBuffer.slice(0, expectedSize));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -27,6 +27,15 @@ describe('Attachment', () => {
|
|||
};
|
||||
assert.strictEqual(Attachment.getFileExtension(input), 'mov');
|
||||
});
|
||||
|
||||
it('should return file extension for application files', () => {
|
||||
const input: Attachment.AttachmentType = {
|
||||
fileName: 'funny-cat.odt',
|
||||
url: 'funny-cat.odt',
|
||||
contentType: MIME.ODT,
|
||||
};
|
||||
assert.strictEqual(Attachment.getFileExtension(input), 'odt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuggestedFilename', () => {
|
||||
|
|
|
@ -361,7 +361,12 @@ export const getSuggestedFilenameSending = ({
|
|||
|
||||
export const getFileExtension = (attachment: AttachmentType): string | undefined => {
|
||||
// we override textplain to the extension of the file
|
||||
if (!attachment.contentType || attachment.contentType === 'text/plain') {
|
||||
// for contenttype starting with application, the mimetype is probably wrong so just use the extension of the file instead
|
||||
if (
|
||||
!attachment.contentType ||
|
||||
attachment.contentType === 'text/plain' ||
|
||||
attachment.contentType.startsWith('application')
|
||||
) {
|
||||
if (attachment.fileName?.length) {
|
||||
const dotLastIndex = attachment.fileName.lastIndexOf('.');
|
||||
if (dotLastIndex !== -1) {
|
||||
|
|
|
@ -14,6 +14,7 @@ export const IMAGE_WEBP = 'image/webp' as MIMEType;
|
|||
export const IMAGE_PNG = 'image/png' as MIMEType;
|
||||
export const VIDEO_MP4 = 'video/mp4' as MIMEType;
|
||||
export const VIDEO_QUICKTIME = 'video/quicktime' as MIMEType;
|
||||
export const ODT = 'application/vnd.oasis.opendocument.spreadsheet' as MIMEType;
|
||||
|
||||
export const isJPEG = (value: MIMEType): boolean => value === 'image/jpeg';
|
||||
export const isImage = (value: MIMEType): boolean =>
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -68,7 +68,7 @@ declare global {
|
|||
setPassword: any;
|
||||
setSettingValue: any;
|
||||
showEditProfileDialog: any;
|
||||
showNicknameDialog: any;
|
||||
showNicknameDialog: (options: { convoId: string }) => void;
|
||||
showResetSessionIdDialog: any;
|
||||
storage: any;
|
||||
textsecure: LibTextsecure;
|
||||
|
|
Loading…
Reference in a new issue