From 123e68c167d8d3246588513966fbd28e4f20971a Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Fri, 15 Oct 2021 10:19:45 +1100 Subject: [PATCH 01/70] WIP: Adding message requests using existing convo list item. --- .../session/LeftPaneMessageSection.tsx | 51 +++++++-- .../session/SessionClosableOverlay.tsx | 106 +++++++++++++++++- ts/models/conversation.ts | 19 ++++ ts/state/ducks/conversations.ts | 1 + ts/test/test-utils/utils/message.ts | 1 + 5 files changed, 164 insertions(+), 14 deletions(-) diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 0aabe6e5d..23a3112eb 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -51,7 +51,7 @@ export type SessionGroupType = SessionComposeToType; interface State { loading: boolean; - overlay: false | SessionComposeToType; + overlay: false | SessionClosableOverlayType; valuePasted: string; } @@ -144,7 +144,7 @@ export class LeftPaneMessageSection extends React.Component { return (
{this.renderHeader()} - {overlay ? this.renderClosableOverlay(overlay) : this.renderConversations()} + {overlay ? this.renderClosableOverlay() : this.renderConversations()}
); } @@ -157,12 +157,18 @@ export class LeftPaneMessageSection extends React.Component { onChange={this.updateSearch} placeholder={window.i18n('searchFor...')} /> +
message requests
{this.renderList()} {this.renderBottomButtons()} ); } + private handleMessageRequestsClick() { + console.warn('handle msg req clicked'); + this.handleToggleOverlay(SessionClosableOverlayType.MessageRequests); + } + public updateSearch(searchTerm: string) { if (!searchTerm) { window.inboxStore?.dispatch(clearSearch()); @@ -201,9 +207,9 @@ export class LeftPaneMessageSection extends React.Component { ); } - private renderClosableOverlay(overlay: SessionComposeToType) { + private renderClosableOverlay() { const { searchTerm, searchResults } = this.props; - const { loading } = this.state; + const { loading, overlay } = this.state; const openGroupElement = ( { /> ); + const messageRequestsElement = ( + { + this.handleToggleOverlay(undefined); + }} + onButtonClick={this.handleMessageButtonClick} + searchTerm={searchTerm} + searchResults={searchResults} + showSpinner={loading} + updateSearch={this.updateSearch} + /> + ); + let overlayElement; switch (overlay) { - case SessionComposeToType.OpenGroup: + case SessionClosableOverlayType.OpenGroup: overlayElement = openGroupElement; break; - case SessionComposeToType.ClosedGroup: + case SessionClosableOverlayType.ClosedGroup: overlayElement = closedGroupElement; break; - default: + case SessionClosableOverlayType.Message: overlayElement = messageElement; + break; + case SessionClosableOverlayType.MessageRequests: + overlayElement = messageRequestsElement; + break; + default: + overlayElement = false; } return overlayElement; @@ -277,7 +304,7 @@ export class LeftPaneMessageSection extends React.Component { buttonType={SessionButtonType.SquareOutline} buttonColor={SessionButtonColor.Green} onClick={() => { - this.handleToggleOverlay(SessionComposeToType.OpenGroup); + this.handleToggleOverlay(SessionClosableOverlayType.OpenGroup); }} /> { buttonType={SessionButtonType.SquareOutline} buttonColor={SessionButtonColor.White} onClick={() => { - this.handleToggleOverlay(SessionComposeToType.ClosedGroup); + this.handleToggleOverlay(SessionClosableOverlayType.ClosedGroup); }} /> ); } - private handleToggleOverlay(conversationType?: SessionComposeToType) { - const overlayState = conversationType || false; + private handleToggleOverlay(overlayType?: SessionClosableOverlayType) { + const overlayState = overlayType || false; this.setState({ overlay: overlayState }); @@ -403,6 +430,6 @@ export class LeftPaneMessageSection extends React.Component { } private handleNewSessionButtonClick() { - this.handleToggleOverlay(SessionComposeToType.Message); + this.handleToggleOverlay(SessionClosableOverlayType.Message); } } diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index cae8021dd..c643762be 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -9,11 +9,18 @@ import { SessionSpinner } from './SessionSpinner'; import { ConversationTypeEnum } from '../../models/conversation'; import { SessionJoinableRooms } from './SessionJoinableDefaultRooms'; import { SpacerLG, SpacerMD } from '../basic/Text'; +import { useSelector } from 'react-redux'; +import { getLeftPaneLists } from '../../state/selectors/conversations'; +import { + ConversationListItemProps, + MemoConversationListItemWithDetails, +} from '../ConversationListItem'; export enum SessionClosableOverlayType { Message = 'message', OpenGroup = 'open-group', ClosedGroup = 'closed-group', + MessageRequests = 'message-requests', } interface Props { @@ -106,6 +113,7 @@ export class SessionClosableOverlay extends React.Component { const isMessageView = overlayMode === SessionClosableOverlayType.Message; const isOpenGroupView = overlayMode === SessionClosableOverlayType.OpenGroup; const isClosedGroupView = overlayMode === SessionClosableOverlayType.ClosedGroup; + const isMessageRequestView = overlayMode === SessionClosableOverlayType.MessageRequests; let title; let buttonText; @@ -133,6 +141,12 @@ export class SessionClosableOverlay extends React.Component { subtitle = window.i18n('createClosedGroupNamePrompt'); placeholder = window.i18n('createClosedGroupPlaceholder'); break; + case SessionClosableOverlayType.MessageRequests: + title = 'Message Requests'; + buttonText = 'requests done'; + subtitle = 'Pending Requests'; + placeholder = 'placeholder'; + break; default: } @@ -172,14 +186,25 @@ export class SessionClosableOverlay extends React.Component { onPressEnter={() => onButtonClick(groupName, selectedMembers)} /> - ) : ( + ) : null} + + {isMessageView ? ( - )} + ) : null} + + {isMessageRequestView ? ( + <> + +
+ + + + ) : null} @@ -266,3 +291,80 @@ export class SessionClosableOverlay extends React.Component { } } } + +const MessageRequestList = () => { + // get all conversations with (accepted / known) + // const convos = useSelector(getConversationLookup); + + const lists = useSelector(getLeftPaneLists); + const conversationx = lists?.conversations as Array; + console.warn({ conversationx }); + + // console.warn({ convos }); + // const allConversations = getConversationController().getConversations(); + // const messageRequests = allConversations.filter(convo => convo.get('isApproved') !== true); + + return ( + <> + {/* {messageRequests.map(convoOfMessage => { */} + {conversationx.map(convoOfMessage => { + return ; + })} + + ); +}; + +// const MessageRequestListItem = (props: { conversation: ConversationModel }) => { +const MessageRequestListItem = (props: { conversation: ConversationListItemProps }) => { + const { conversation } = props; + // const { id: conversationId } = conversation; + + // TODO: add hovering + // TODO: add styling + + /** + * open the conversation selected + */ + // const openConvo = useCallback( + // async (e: React.MouseEvent) => { + // // mousedown is invoked sooner than onClick, but for both right and left click + // if (e.button === 0) { + // await openConversationWithMessages({ conversationKey: conversationId }); + // } + // }, + // [conversationId] + // ); + + // /** + // * show basic highlight animation + // */ + // const handleMouseOver = () => { + // console.warn('hovered'); + // }; + + return ( + //
{ + // e.stopPropagation(); + // e.preventDefault(); + // }} + // // className="message-request__item" + + // // className={classNames( + // // 'module-conversation-list-item', + // // unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, + // // unreadCount && unreadCount > 0 && mentionedUs + // // ? 'module-conversation-list-item--mentioned-us' + // // : null, + // // isSelected ? 'module-conversation-list-item--is-selected' : null, + // // isBlocked ? 'module-conversation-list-item--is-blocked' : null + // // )} + // > + // {conversation.getName()} + //
+ + + ); +}; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index dfc68c9df..3704170a2 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -104,6 +104,7 @@ export interface ConversationAttributes { triggerNotificationsFor: ConversationNotificationSettingType; isTrustedForAttachmentDownload: boolean; isPinned: boolean; + isApproved: boolean; } export interface ConversationAttributesOptionals { @@ -144,6 +145,7 @@ export interface ConversationAttributesOptionals { triggerNotificationsFor?: ConversationNotificationSettingType; isTrustedForAttachmentDownload?: boolean; isPinned: boolean; + isApproved: boolean; } /** @@ -433,6 +435,7 @@ export class ConversationModel extends Backbone.Model { const isBlocked = this.isBlocked(); const subscriberCount = this.get('subscriberCount'); const isPinned = this.isPinned(); + const isApproved = this.isApproved(); const hasNickname = !!this.getNickname(); const isKickedFromGroup = !!this.get('isKickedFromGroup'); const left = !!this.get('left'); @@ -508,6 +511,9 @@ export class ConversationModel extends Backbone.Model { if (isPinned) { toRet.isPinned = isPinned; } + if (isApproved) { + toRet.isApproved = isApproved; + } if (subscriberCount) { toRet.subscriberCount = subscriberCount; } @@ -1375,6 +1381,15 @@ export class ConversationModel extends Backbone.Model { } } + public async setIsApproved(value: boolean) { + if (value !== this.get('isApproved')) { + this.set({ + isApproved: true, + }); + await this.commit(); + } + } + public async setGroupName(name: string) { const profileName = this.get('name'); if (profileName !== name) { @@ -1482,6 +1497,10 @@ export class ConversationModel extends Backbone.Model { return this.get('isPinned'); } + public isApproved() { + return this.get('isApproved'); + } + public getTitle() { if (this.isPrivate()) { const profileName = this.getProfileName(); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 789d5974e..0de58628c 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -243,6 +243,7 @@ export interface ReduxConversationType { currentNotificationSetting?: ConversationNotificationSettingType; isPinned?: boolean; + isApproved?: boolean; } export interface NotificationForConvoOption { diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index b45ac39ef..de5d4c7a6 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -95,6 +95,7 @@ export class MockConversation { triggerNotificationsFor: 'all', isTrustedForAttachmentDownload: false, isPinned: false, + isApproved: false, }; } From c3f20aceb2dba8ca8e7eacec943c57902db10ccf Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Wed, 20 Oct 2021 17:49:14 +1100 Subject: [PATCH 02/70] WIP message requesting. Banner styling finished. --- stylesheets/_session_left_pane.scss | 8 ++ ts/components/ConversationListItem.tsx | 41 +++++++- .../session/LeftPaneMessageSection.tsx | 14 ++- .../session/MessageRequestsBanner.tsx | 98 +++++++++++++++++++ .../session/SessionClosableOverlay.tsx | 75 +++----------- .../session/settings/SessionSettings.tsx | 27 ++++- ts/models/conversation.ts | 9 +- .../conversations/ConversationController.ts | 1 + ts/state/ducks/userConfig.tsx | 15 ++- 9 files changed, 217 insertions(+), 71 deletions(-) create mode 100644 ts/components/session/MessageRequestsBanner.tsx diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index ade13d4d4..982d7a4d7 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -249,6 +249,14 @@ $session-compose-margin: 20px; margin-bottom: 3rem; flex-shrink: 0; } + + .message-request-list__container { + width: 100%; + + .session-button { + margin: $session-margin-xs 0; + } + } } } .module-search-results { diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index cdb1921f3..99d1a1d07 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -27,6 +27,10 @@ import { useSelector } from 'react-redux'; import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; +import { Flex } from './basic/Flex'; +import { SessionButton } from './session/SessionButton'; +import { getConversationById } from '../data/data'; +import { getConversationController } from '../session/conversations'; // tslint:disable-next-line: no-empty-interface export interface ConversationListItemProps extends ReduxConversationType {} @@ -42,6 +46,7 @@ export const StyledConversationListItemIconWrapper = styled.div` type PropsHousekeeping = { style?: Object; + isMessageRequest?: boolean; }; // tslint:disable: use-simple-attributes @@ -261,6 +266,7 @@ const ConversationListItem = (props: Props) => { avatarPath, isPrivate, currentNotificationSetting, + isMessageRequest, } = props; const triggerId = `conversation-item-${conversationId}-ctxmenu`; const key = `conversation-item-${conversationId}`; @@ -277,15 +283,32 @@ const ConversationListItem = (props: Props) => { [conversationId] ); + /** + * deletes the conversation + */ + const handleConversationDecline = async () => { + await getConversationController().deleteContact(conversationId); + }; + + /** + * marks the conversation as approved. + */ + const handleConversationAccept = async () => { + const convo = await getConversationById(conversationId); + convo?.setIsApproved(true); + console.warn('convo marked as approved'); + console.warn({ convo }); + }; + return (
{ - e.stopPropagation(); - e.preventDefault(); - }} + // onMouseUp={e => { + // e.stopPropagation(); + // e.preventDefault(); + // }} onContextMenu={(e: any) => { contextMenu.show({ id: triggerId, @@ -327,6 +350,16 @@ const ConversationListItem = (props: Props) => { unreadCount={unreadCount || 0} lastMessage={lastMessage} /> + {isMessageRequest ? ( + + Decline + Accept + + ) : null}
diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 23a3112eb..fc6871017 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -28,6 +28,7 @@ import { onsNameRegex } from '../../session/snode_api/SNodeAPI'; import { SNodeAPI } from '../../session/snode_api'; import { clearSearch, search, updateSearchTerm } from '../../state/ducks/search'; import _ from 'lodash'; +import { MessageRequestsBanner } from './MessageRequestsBanner'; export interface Props { searchTerm: string; @@ -95,6 +96,15 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('render: must provided conversations if no search results are provided'); } + // TODO: make selectors for this instead. + // TODO: only filter conversations if setting for requests is applied + const approvedConversations = conversations.filter(c => c.isApproved === true); + console.warn({ approvedConversations }); + const messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true; + + const conversationsForList = messageRequestsEnabled ? approvedConversations : conversations; + const length = conversations.length; const listKey = 0; // Note: conversations is not a known prop for List, but it is required to ensure that @@ -106,7 +116,7 @@ export class LeftPaneMessageSection extends React.Component { {({ height, width }) => ( { onChange={this.updateSearch} placeholder={window.i18n('searchFor...')} /> -
message requests
+ {this.renderList()} {this.renderBottomButtons()} diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx new file mode 100644 index 000000000..8e5fcb2ce --- /dev/null +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { getLeftPaneLists } from '../../state/selectors/conversations'; +import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; + +const StyledMessageRequestBanner = styled.div` + border-left: var(--border-unread); + height: 64px; + width: 100%; + max-width: 300px; + display: flex; + flex-direction: row; + padding: 8px 16px; + align-items: center; + cursor: pointer; + + transition: var(--session-transition-duration); + + &:hover { + background: var(--color-clickable-hovered); + } +`; + +const StyledMessageRequestBannerHeader = styled.span` + font-weight: bold; + font-size: 15px; + color: var(--color-text-subtle); + padding-left: var(--margin-xs); + margin-inline-start: 12px; + margin-top: var(--margin-sm); + line-height: 18px; + overflow-x: hidden; + overflow-y: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const StyledCircleIcon = styled.div` + padding-left: var(--margin-xs); +`; + +const StyledUnreadCounter = styled.div` + font-weight: bold; + border-radius: 50%; + background-color: var(--color-clickable-hovered); + margin-left: 10px; + width: 20px; + height: 20px; + line-height: 25px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +`; + +const StyledGridContainer = styled.div` + border: solid 1px black; + display: flex; + width: 36px; + height: 36px; + align-items: center; + border-radius: 50%; + justify-content: center; + background-color: var(--color-conversation-item-has-unread); +`; + +export const CirclularIcon = (props: { iconType: SessionIconType; iconSize: SessionIconSize }) => { + const { iconSize, iconType } = props; + + return ( + + + + + + ); +}; + +export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { + const { handleOnClick } = props; + const convos = useSelector(getLeftPaneLists).conversations; + const pendingRequests = convos.filter(c => c.isApproved !== true) || []; + + return ( + + + Message Requests + +
{pendingRequests.length}
+
+
+ ); +}; diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index c643762be..a396d62ca 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -292,79 +292,30 @@ export class SessionClosableOverlay extends React.Component { } } + + const MessageRequestList = () => { // get all conversations with (accepted / known) - // const convos = useSelector(getConversationLookup); - const lists = useSelector(getLeftPaneLists); - const conversationx = lists?.conversations as Array; - console.warn({ conversationx }); - - // console.warn({ convos }); - // const allConversations = getConversationController().getConversations(); - // const messageRequests = allConversations.filter(convo => convo.get('isApproved') !== true); - + const unapprovedConversations = lists?.conversations.filter(c => { + return !c.isApproved; + }) as Array; return ( - <> - {/* {messageRequests.map(convoOfMessage => { */} - {conversationx.map(convoOfMessage => { - return ; +
+ {unapprovedConversations.map(conversation => { + return ; })} - +
); }; // const MessageRequestListItem = (props: { conversation: ConversationModel }) => { const MessageRequestListItem = (props: { conversation: ConversationListItemProps }) => { const { conversation } = props; - // const { id: conversationId } = conversation; - - // TODO: add hovering - // TODO: add styling - - /** - * open the conversation selected - */ - // const openConvo = useCallback( - // async (e: React.MouseEvent) => { - // // mousedown is invoked sooner than onClick, but for both right and left click - // if (e.button === 0) { - // await openConversationWithMessages({ conversationKey: conversationId }); - // } - // }, - // [conversationId] - // ); - - // /** - // * show basic highlight animation - // */ - // const handleMouseOver = () => { - // console.warn('hovered'); - // }; - return ( - //
{ - // e.stopPropagation(); - // e.preventDefault(); - // }} - // // className="message-request__item" - - // // className={classNames( - // // 'module-conversation-list-item', - // // unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, - // // unreadCount && unreadCount > 0 && mentionedUs - // // ? 'module-conversation-list-item--mentioned-us' - // // : null, - // // isSelected ? 'module-conversation-list-item--is-selected' : null, - // // isBlocked ? 'module-conversation-list-item--is-blocked' : null - // // )} - // > - // {conversation.getName()} - //
- - + ); }; diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 05db39c86..5c6228ee0 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -17,7 +17,11 @@ import { import { shell } from 'electron'; import { mapDispatchToProps } from '../../../state/actions'; import { unblockConvoById } from '../../../interactions/conversationInteractions'; -import { toggleAudioAutoplay } from '../../../state/ducks/userConfig'; +import { + disableMessageRequests, + enableMessageRequests, + toggleAudioAutoplay, +} from '../../../state/ducks/userConfig'; import { sessionPassword, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { PasswordAction } from '../../dialog/SessionPasswordDialog'; import { SessionIconButton } from '../icon'; @@ -406,7 +410,28 @@ class SettingsViewInner extends React.Component { comparisonValue: undefined, onClick: undefined, }, + { + id: 'message-request-setting', + title: 'Message Requests', // TODO: translation + description: 'Enable Message Request Inbox', + hidden: false, + type: SessionSettingType.Toggle, + category: SessionSettingCategory.Appearance, + setFn: () => { + window.inboxStore?.dispatch(toggleAudioAutoplay()); + if (window.inboxStore?.getState().userConfig.messageRequests) { + window.inboxStore?.dispatch(disableMessageRequests()); + } else { + window.inboxStore?.dispatch(enableMessageRequests()); + } + }, + content: { + defaultValue: window.inboxStore?.getState().userConfig.audioAutoplay, + }, + comparisonValue: undefined, + onClick: undefined, + }, { id: 'notification-setting', title: window.i18n('notificationSettingsDialog'), diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 3704170a2..8919c9afe 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -714,6 +714,12 @@ export class ConversationModel extends Backbone.Model { const sentAt = message.get('sent_at'); + // TODO: for debuggong + if (message.get('body')?.includes('unapprove')) { + console.warn('setting to unapprove'); + await this.setIsApproved(false); + } + if (!sentAt) { throw new Error('sendMessageJob() sent_at must be set.'); } @@ -771,6 +777,7 @@ export class ConversationModel extends Backbone.Model { const chatMessagePrivate = new VisibleMessage(chatMessageParams); await getMessageQueue().sendToPubKey(destinationPubkey, chatMessagePrivate); + // this.setIsApproved(true); // consider the conversation approved even if the message fails to send return; } @@ -1384,7 +1391,7 @@ export class ConversationModel extends Backbone.Model { public async setIsApproved(value: boolean) { if (value !== this.get('isApproved')) { this.set({ - isApproved: true, + isApproved: value, }); await this.commit(); } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 08f3e39a0..3e0988f87 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -99,6 +99,7 @@ export class ConversationController { const create = async () => { try { + debugger; await saveConversation(conversation.attributes); } catch (error) { window?.log?.error( diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index af64b2cb4..ea34b9cfd 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -7,11 +7,13 @@ import { createSlice } from '@reduxjs/toolkit'; export interface UserConfigState { audioAutoplay: boolean; showRecoveryPhrasePrompt: boolean; + messageRequests: boolean; } export const initialUserConfigState = { audioAutoplay: false, showRecoveryPhrasePrompt: true, + messageRequests: true, }; const userConfigSlice = createSlice({ @@ -24,9 +26,20 @@ const userConfigSlice = createSlice({ disableRecoveryPhrasePrompt: state => { state.showRecoveryPhrasePrompt = false; }, + disableMessageRequests: state => { + state.messageRequests = false; + }, + enableMessageRequests: state => { + state.messageRequests = true; + }, }, }); const { actions, reducer } = userConfigSlice; -export const { toggleAudioAutoplay, disableRecoveryPhrasePrompt } = actions; +export const { + toggleAudioAutoplay, + disableRecoveryPhrasePrompt, + disableMessageRequests, + enableMessageRequests, +} = actions; export const userConfigReducer = reducer; From dcfa286d771b21b1a03e7ff1b4d3bbdaae103add Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Fri, 22 Oct 2021 12:12:59 +1100 Subject: [PATCH 03/70] WIP: fixing missing spaces for list items that have been removed. --- ts/components/ConversationListItem.tsx | 30 ++++++++++---- ts/components/LeftPane.tsx | 2 + .../session/LeftPaneMessageSection.tsx | 40 +++++++++++++++++-- .../session/MessageRequestsBanner.tsx | 8 +++- .../session/SessionClosableOverlay.tsx | 3 -- ts/models/conversation.ts | 12 +++++- ts/state/selectors/conversations.ts | 16 ++++++++ 7 files changed, 93 insertions(+), 18 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 99d1a1d07..47457e4da 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -28,7 +28,7 @@ import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; import { Flex } from './basic/Flex'; -import { SessionButton } from './session/SessionButton'; +import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { getConversationById } from '../data/data'; import { getConversationController } from '../session/conversations'; @@ -294,10 +294,10 @@ const ConversationListItem = (props: Props) => { * marks the conversation as approved. */ const handleConversationAccept = async () => { - const convo = await getConversationById(conversationId); - convo?.setIsApproved(true); + const conversationToApprove = await getConversationById(conversationId); + conversationToApprove?.setIsApproved(true); console.warn('convo marked as approved'); - console.warn({ convo }); + console.warn({ convo: conversationToApprove }); }; return ( @@ -351,14 +351,24 @@ const ConversationListItem = (props: Props) => { lastMessage={lastMessage} /> {isMessageRequest ? ( - - Decline - Accept - + + Decline + + + Accept + + ) : null} @@ -381,4 +391,8 @@ const ConversationListItem = (props: Props) => { ); }; +const StyledButtonContainer = styled(Flex)` + justify-content: space-evenly; +`; + export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual); diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 3d9bf780d..56bfe79bc 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -31,6 +31,8 @@ const InnerLeftPaneMessageSection = () => { const lists = showSearch ? undefined : useSelector(getLeftPaneLists); // tslint:disable: use-simple-attributes + // const + return ( { @@ -62,25 +64,42 @@ export class LeftPaneMessageSection extends React.Component { public constructor(props: Props) { super(props); + const approvedConversations = props.conversations?.filter(convo => Boolean(convo.isApproved)); + const unapprovedConversations = props.conversations?.filter( + convo => !Boolean(convo.isApproved) + ); + this.state = { loading: false, overlay: false, valuePasted: '', + approvedConversations: approvedConversations || [], + unapprovedConversations: unapprovedConversations || [], }; autoBind(this); this.debouncedSearch = _.debounce(this.search.bind(this), 20); } - public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element => { + public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => { const { conversations } = this.props; + // const { approvedConversations: conversations } = this.state; + console.warn({ conversations }); if (!conversations) { throw new Error('renderRow: Tried to render without conversations'); } - const conversation = conversations[index]; + console.warn(`${index}`); + console.warn({ conversation }); + + // TODO: need to confirm whats best here. + // true by default then false on newly received or + // false by default and true when approved but then how to handle pre-existing convos? + if (conversation.isApproved === undefined || conversation.isApproved === false) { + return null; + } return ; }; @@ -99,13 +118,21 @@ export class LeftPaneMessageSection extends React.Component { // TODO: make selectors for this instead. // TODO: only filter conversations if setting for requests is applied const approvedConversations = conversations.filter(c => c.isApproved === true); - console.warn({ approvedConversations }); const messageRequestsEnabled = window.inboxStore?.getState().userConfig.messageRequests === true; const conversationsForList = messageRequestsEnabled ? approvedConversations : conversations; + if (!this.state.approvedConversations.length) { + this.setState({ + approvedConversations: conversationsForList, + }); + } + + console.warn({ conversationsForList }); + + // const length = conversations.length; + const length = conversationsForList.length; - const length = conversations.length; const listKey = 0; // Note: conversations is not a known prop for List, but it is required to ensure that // it re-renders when our conversation data changes. Otherwise it would just render @@ -120,6 +147,7 @@ export class LeftPaneMessageSection extends React.Component { height={height} rowCount={length} rowHeight={64} + // rowHeight={this.getRowHeight} rowRenderer={this.renderRow} width={width} autoHeight={false} @@ -132,6 +160,10 @@ export class LeftPaneMessageSection extends React.Component { return [list]; } + // private getRowHeight({index: any}: any) { + // if (this.) + // } + public closeOverlay({ pubKey }: { pubKey: string }) { if (this.state.valuePasted === pubKey) { this.setState({ overlay: false, valuePasted: '' }); diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index 8e5fcb2ce..e0e87bcfa 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -84,14 +84,18 @@ export const CirclularIcon = (props: { iconType: SessionIconType; iconSize: Sess export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { const { handleOnClick } = props; const convos = useSelector(getLeftPaneLists).conversations; - const pendingRequests = convos.filter(c => c.isApproved !== true) || []; + const pendingRequestsCount = (convos.filter(c => c.isApproved !== true) || []).length; + + if (!pendingRequestsCount) { + return null; + } return ( Message Requests -
{pendingRequests.length}
+
{pendingRequestsCount}
); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index a396d62ca..371b0a00a 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -131,7 +131,6 @@ export class SessionClosableOverlay extends React.Component { case 'open-group': title = window.i18n('joinOpenGroup'); buttonText = window.i18n('next'); - // descriptionLong = ''; subtitle = window.i18n('openGroupURL'); placeholder = window.i18n('enterAnOpenGroupURL'); break; @@ -292,8 +291,6 @@ export class SessionClosableOverlay extends React.Component { } } - - const MessageRequestList = () => { // get all conversations with (accepted / known) const lists = useSelector(getLeftPaneLists); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 8919c9afe..78e785bde 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -714,7 +714,7 @@ export class ConversationModel extends Backbone.Model { const sentAt = message.get('sent_at'); - // TODO: for debuggong + // TODO: for debugging if (message.get('body')?.includes('unapprove')) { console.warn('setting to unapprove'); await this.setIsApproved(false); @@ -1389,11 +1389,21 @@ export class ConversationModel extends Backbone.Model { } public async setIsApproved(value: boolean) { + console.warn(`Setting ${this.attributes.nickname} isApproved to:: ${value}`); if (value !== this.get('isApproved')) { this.set({ isApproved: value, }); await this.commit(); + + if (window?.inboxStore) { + window.inboxStore?.dispatch( + conversationChanged({ + id: this.id, + data: this.getConversationModelProps(), + }) + ); + } } } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index aecbdf0ec..7a5e4a580 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -343,6 +343,22 @@ export const getLeftPaneLists = createSelector( _getLeftPaneLists ); +export const getApprovedConversations = createSelector( + getConversationLookup, + (lookup: ConversationLookupType): Array => { + return Object.values(lookup).filter(convo => convo.isApproved === true); + } +); + +export const getUnapprovedConversations = createSelector( + getConversationLookup, + (lookup: ConversationLookupType): Array => { + return Object.values(lookup).filter( + convo => convo.isApproved === false || convo.isApproved === undefined + ); + } +); + export const getMe = createSelector( [getConversationLookup, getOurNumber], (lookup: ConversationLookupType, ourNumber: string): ReduxConversationType => { From e405b5ffd974fd7fa69067b3dfe99294e6f4d5da Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Mon, 25 Oct 2021 17:33:37 +1100 Subject: [PATCH 04/70] git stash --- ts/components/ConversationListItem.tsx | 1 + ts/components/session/LeftPaneMessageSection.tsx | 8 +++++++- ts/components/session/MessageRequestsBanner.tsx | 2 +- ts/components/session/SessionClosableOverlay.tsx | 2 +- ts/components/session/icon/Icons.tsx | 7 +++++++ ts/session/utils/syncUtils.ts | 4 +++- 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 47457e4da..e071f6408 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -298,6 +298,7 @@ const ConversationListItem = (props: Props) => { conversationToApprove?.setIsApproved(true); console.warn('convo marked as approved'); console.warn({ convo: conversationToApprove }); + conversationToApprove.sendS }; return ( diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index e4d1863bf..50fc1b4e8 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -306,7 +306,13 @@ export class LeftPaneMessageSection extends React.Component { onCloseClick={() => { this.handleToggleOverlay(undefined); }} - onButtonClick={this.handleMessageButtonClick} + onButtonClick={async () => { + // decline all convos + // close modal + // this.state.approvedConversations.map(async(convo) => { + console.warn('Test'); + // } ) + }} searchTerm={searchTerm} searchResults={searchResults} showSpinner={loading} diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index e0e87bcfa..c375ea926 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -92,7 +92,7 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { return ( - + Message Requests
{pendingRequestsCount}
diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 371b0a00a..66b059808 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -142,7 +142,7 @@ export class SessionClosableOverlay extends React.Component { break; case SessionClosableOverlayType.MessageRequests: title = 'Message Requests'; - buttonText = 'requests done'; + buttonText = 'Decline All'; subtitle = 'Pending Requests'; placeholder = 'placeholder'; break; diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index de70a859c..45ab607fe 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -25,6 +25,7 @@ export type SessionIconType = | 'info' | 'link' | 'lock' + | 'messageRequest' | 'microphone' | 'moon' | 'mute' @@ -223,6 +224,12 @@ export const icons = { viewBox: '0 0 512 512', ratio: 1, }, + messageRequest: { + path: + 'M68.987 7.718H27.143c-2.73 0-5.25.473-7.508 1.417-2.257.945-4.357 2.363-6.248 4.253-1.89 1.89-3.308 3.99-4.253 6.248-.945 2.257-1.417 4.778-1.417 7.508V67.99c0 2.73.472 5.25 1.417 7.508.945 2.258 2.363 4.357 4.253 6.248 1.942 1.891 4.043 3.359 6.3 4.252 2.258.945 4.726 1.418 7.456 1.418h17.956c2.101 0 3.833 1.732 3.833 3.832 0 .473-.105.893-.21 1.313-.683 2.521-1.418 5.041-2.258 7.455-.893 2.574-1.837 4.988-2.888 7.352-.525 1.207-1.155 2.361-1.837 3.57 3.675-1.629 7.14-3.518 10.343-5.619 3.36-2.205 6.51-4.672 9.397-7.35 2.94-2.73 5.565-5.723 7.98-8.926.735-.996 1.89-1.521 3.045-1.521H87.94c2.73 0 5.198-.473 7.455-1.418 2.258-.945 4.358-2.363 6.301-4.252 1.89-1.891 3.308-3.99 4.253-6.248.944-2.258 1.417-4.779 1.417-7.508V27.249c0-2.73-.473-5.25-1.417-7.508-.945-2.258-2.363-4.357-4.253-6.248s-3.99-3.308-6.248-4.252c-2.258-.945-4.777-1.418-7.508-1.418H68.987v-.105zm-7.282 47.97h-9.976V54.61c0-1.833.188-3.327.574-4.471.386-1.155.958-2.193 1.721-3.143.762-.951 2.474-2.619 5.136-5.005 1.416-1.251 2.124-2.396 2.124-3.435 0-1.047-.287-1.852-.851-2.434-.574-.573-1.435-.864-2.59-.864-1.247 0-2.269.446-3.083 1.338-.816.883-1.335 2.444-1.561 4.657l-10.191-1.368c.349-4.054 1.711-7.314 4.078-9.787 2.376-2.473 6.015-3.706 10.917-3.706 3.818 0 6.893.863 9.24 2.58 3.184 2.338 4.778 5.441 4.778 9.321 0 1.61-.412 3.172-1.237 4.666-.815 1.493-2.501 3.327-5.037 5.48-1.766 1.523-2.887 2.735-3.353 3.657-.456.914-.689 2.116-.689 3.592zm-10.325 2.87h10.693v8.532H51.38v-8.532zM46.097.053H87.94c3.675 0 7.141.683 10.396 1.995 3.202 1.312 6.143 3.308 8.768 5.933 2.626 2.625 4.621 5.565 5.934 8.768 1.312 3.203 1.994 6.667 1.994 10.396V67.99c0 3.729-.683 7.193-1.994 10.396-1.313 3.201-3.308 6.141-5.934 8.768-2.625 2.625-5.565 4.566-8.768 5.932-3.202 1.313-6.668 1.996-10.396 1.996H74.395c-2.362 2.992-4.935 5.826-7.665 8.4-3.255 3.045-6.72 5.773-10.448 8.189-3.728 2.467-7.718 4.621-11.971 6.457-4.2 1.838-8.715 3.361-13.44 4.621-1.365.367-2.835-.053-3.833-1.156-1.417-1.574-1.26-3.988.315-5.406 2.205-1.943 4.095-3.938 5.618-5.934 1.47-1.941 2.678-3.938 3.57-5.984v-.053c.998-2.205 1.89-4.463 2.678-6.721.263-.787.525-1.627.788-2.467H27.091c-3.675 0-7.14-.684-10.396-1.996-3.203-1.313-6.143-3.307-8.768-5.932-2.625-2.625-4.62-5.566-5.933-8.768C.682 75.078 0 71.613 0 67.938V27.091c0-3.676.682-7.141 1.995-10.396 1.313-3.203 3.308-6.143 5.933-8.768 2.625-2.625 5.565-4.62 8.768-5.933S23.363 0 27.091 0h18.953l.053.053z', + viewBox: '0 0 115.031 122.88', + ratio: 1, + }, microphone: { path: 'M43.362728,18.444286 C46.0752408,18.444286 48.2861946,16.2442453 48.2861946,13.5451212 L48.2861946,6.8991648 C48.2861946,4.20004074 46.0752408,2 43.362728,2 C40.6502153,2 38.4392615,4.20004074 38.4392615,6.8991648 L38.4392615,13.5451212 C38.4392615,16.249338 40.6502153,18.444286 43.362728,18.444286 Z M51.0908304,12.9238134 C51.4388509,12.9238134 51.7203381,13.2039112 51.7203381,13.5502139 C51.7203381,17.9248319 48.3066664,21.5202689 43.9871178,21.8411082 L43.9871178,21.8411082 L43.9871178,25.747199 L47.2574869,25.747199 C47.6055074,25.747199 47.8869946,26.0272968 47.8869946,26.3735995 C47.8869946,26.7199022 47.6055074,27 47.2574869,27 L47.2574869,27 L39.4628512,27 C39.1148307,27 38.8333435,26.7199022 38.8333435,26.3735995 C38.8333435,26.0272968 39.1148307,25.747199 39.4628512,25.747199 L39.4628512,25.747199 L42.7332204,25.747199 L42.7332204,21.8411082 C38.4136717,21.5253616 35,17.9248319 35,13.5502139 C35,13.2039112 35.2814872,12.9238134 35.6295077,12.9238134 C35.9775282,12.9238134 36.2538974,13.2039112 36.2436615,13.5502139 C36.2436615,17.4512121 39.4321435,20.623956 43.3524921,20.623956 C47.2728408,20.623956 50.4613228,17.4512121 50.4613228,13.5502139 C50.4613228,13.2039112 50.7428099,12.9238134 51.0908304,12.9238134 Z M43.362728,3.24770829 C45.3843177,3.24770829 47.0322972,4.88755347 47.0322972,6.8991648 L47.0322972,13.5451212 C47.0322972,15.5567325 45.3843177,17.1965777 43.362728,17.1965777 C41.3411383,17.1965777 39.6931589,15.5567325 39.6931589,13.5451212 L39.6931589,6.8991648 C39.6931589,4.88755347 41.3411383,3.24770829 43.362728,3.24770829', diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index c4cac9648..11eeaaad6 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -200,7 +200,9 @@ const getValidContacts = (convos: Array) => { return _.compact(contacts); }; -export const getCurrentConfigurationMessage = async (convos: Array) => { +export const getCurrentConfigurationMessage = async ( + convos: Array +): Promise => { const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); const ourConvo = convos.find(convo => convo.id === ourPubKey); From b6c1578262ea2a59ba63c47dbde492fc09348d49 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 26 Oct 2021 09:21:37 +1100 Subject: [PATCH 05/70] WIP message request adding todo note. --- ts/components/ConversationListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index e071f6408..b8de0c397 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -298,7 +298,7 @@ const ConversationListItem = (props: Props) => { conversationToApprove?.setIsApproved(true); console.warn('convo marked as approved'); console.warn({ convo: conversationToApprove }); - conversationToApprove.sendS + // TODO: Send sync message to other devices. Using config message }; return ( From 84e12ff42fe9969bc5dda14de91f2ae505556439 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 26 Oct 2021 10:57:51 +1100 Subject: [PATCH 06/70] Alter request button item positioning. --- stylesheets/_session_left_pane.scss | 2 +- ts/components/ConversationListItem.tsx | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index 982d7a4d7..f33fba1df 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -254,7 +254,7 @@ $session-compose-margin: 20px; width: 100%; .session-button { - margin: $session-margin-xs 0; + margin: $session-margin-xs $session-margin-xs $session-margin-xs 0; } } } diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index b8de0c397..61a2fb67b 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -352,10 +352,11 @@ const ConversationListItem = (props: Props) => { lastMessage={lastMessage} /> {isMessageRequest ? ( - { > Accept - + ) : null} @@ -392,8 +393,4 @@ const ConversationListItem = (props: Props) => { ); }; -const StyledButtonContainer = styled(Flex)` - justify-content: space-evenly; -`; - export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual); From 116cb25b275e6afb2a2fc2effc2649139aea3960 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 26 Oct 2021 11:10:54 +1100 Subject: [PATCH 07/70] fix icon position --- ts/components/session/MessageRequestsBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index c375ea926..8f319fb8d 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -11,7 +11,7 @@ const StyledMessageRequestBanner = styled.div` max-width: 300px; display: flex; flex-direction: row; - padding: 8px 16px; + padding: 8px 12px; // adjusting for unread border always being active align-items: center; cursor: pointer; From d57300688ec745a939d0bb41996dc5d33c2a23a6 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 26 Oct 2021 22:56:22 +1100 Subject: [PATCH 08/70] no longer showing empty space for conversations moved from list. --- ts/components/ConversationListItem.tsx | 2 ++ .../session/LeftPaneMessageSection.tsx | 33 +++++++++---------- .../session/SessionClosableOverlay.tsx | 1 + 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 61a2fb67b..f466f7170 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -299,6 +299,8 @@ const ConversationListItem = (props: Props) => { console.warn('convo marked as approved'); console.warn({ convo: conversationToApprove }); // TODO: Send sync message to other devices. Using config message + + }; return ( diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 50fc1b4e8..e1d57a2c1 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -69,6 +69,8 @@ export class LeftPaneMessageSection extends React.Component { convo => !Boolean(convo.isApproved) ); + console.warn('convos updated'); + this.state = { loading: false, overlay: false, @@ -83,21 +85,22 @@ export class LeftPaneMessageSection extends React.Component { public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => { const { conversations } = this.props; - // const { approvedConversations: conversations } = this.state; - console.warn({ conversations }); - - if (!conversations) { + const approvedConversations = conversations?.filter(c => c.isApproved === true); + if (!conversations || !approvedConversations) { throw new Error('renderRow: Tried to render without conversations'); } - const conversation = conversations[index]; - - console.warn(`${index}`); - console.warn({ conversation }); + // const conversation = conversations[index]; + let conversation; + if (approvedConversations?.length) { + conversation = approvedConversations[index]; + } // TODO: need to confirm whats best here. - // true by default then false on newly received or - // false by default and true when approved but then how to handle pre-existing convos? - if (conversation.isApproved === undefined || conversation.isApproved === false) { + if ( + !conversation || + conversation?.isApproved === undefined || + conversation.isApproved === false + ) { return null; } return ; @@ -143,11 +146,11 @@ export class LeftPaneMessageSection extends React.Component { {({ height, width }) => ( { return [list]; } - // private getRowHeight({index: any}: any) { - // if (this.) - // } - public closeOverlay({ pubKey }: { pubKey: string }) { if (this.state.valuePasted === pubKey) { this.setState({ overlay: false, valuePasted: '' }); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 66b059808..9536fead5 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -297,6 +297,7 @@ const MessageRequestList = () => { const unapprovedConversations = lists?.conversations.filter(c => { return !c.isApproved; }) as Array; + console.warn({ unapprovedConversationsListConstructor: unapprovedConversations }); return (
{unapprovedConversations.map(conversation => { From 9e0f128fc67860a7516bfd80a73d215fad2520fd Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Wed, 27 Oct 2021 10:36:22 +1100 Subject: [PATCH 09/70] Adding isApproved field to protobuf. --- protos/SignalService.proto | 1 + .../messages/outgoing/controlMessage/ConfigurationMessage.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 439b5b91f..0d3658963 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -170,6 +170,7 @@ message ConfigurationMessage { required string name = 2; optional string profilePicture = 3; optional bytes profileKey = 4; + optional bool isApproved = 5; } repeated ClosedGroup closedGroups = 1; diff --git a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts index 82c6845db..89f8b710d 100644 --- a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts @@ -93,22 +93,26 @@ export class ConfigurationMessageContact { public displayName: string; public profilePictureURL?: string; public profileKey?: Uint8Array; + public isApproved?: boolean; public constructor({ publicKey, displayName, profilePictureURL, profileKey, + isApproved, }: { publicKey: string; displayName: string; profilePictureURL?: string; profileKey?: Uint8Array; + isApproved?: boolean; }) { this.publicKey = publicKey; this.displayName = displayName; this.profilePictureURL = profilePictureURL; this.profileKey = profileKey; + this.isApproved = isApproved; // will throw if public key is invalid PubKey.cast(publicKey); @@ -131,6 +135,7 @@ export class ConfigurationMessageContact { name: this.displayName, profilePicture: this.profilePictureURL, profileKey: this.profileKey, + isApproved: this.isApproved, }); } } From 4ad14e4c5b76ebe5ad54c7b5d4f75effcec1fc05 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Fri, 29 Oct 2021 10:58:40 +1100 Subject: [PATCH 10/70] Added syncing accepting of contact between running instances. --- app/sql.js | 7 ++++ ts/components/ConversationListItem.tsx | 15 ++++--- .../session/LeftPaneMessageSection.tsx | 40 ++++++++----------- ts/data/data.ts | 4 +- ts/models/conversation.ts | 13 +----- ts/receiver/configMessage.ts | 21 +++++----- ts/receiver/contentMessage.ts | 1 + .../conversations/ConversationController.ts | 1 - ts/session/utils/syncUtils.ts | 16 ++++++-- 9 files changed, 62 insertions(+), 56 deletions(-) diff --git a/app/sql.js b/app/sql.js index 217f7ba56..159d760cf 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1600,9 +1600,14 @@ function updateConversation(data) { type, members, name, + isApproved, profileName, } = data; + console.log({ usrData: data }); + console.log({ usrDataTrace: console.trace() }); + console.log('usrData@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'); + globalInstance .prepare( `UPDATE ${CONVERSATIONS_TABLE} SET @@ -1612,6 +1617,7 @@ function updateConversation(data) { type = $type, members = $members, name = $name, + isApproved = $isApproved, profileName = $profileName WHERE id = $id;` ) @@ -1623,6 +1629,7 @@ function updateConversation(data) { type, members: members ? members.join(' ') : null, name, + isApproved, profileName, }); } diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index f466f7170..6991e5ff1 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -30,7 +30,7 @@ import { ConversationNotificationSettingType } from '../models/conversation'; import { Flex } from './basic/Flex'; import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { getConversationById } from '../data/data'; -import { getConversationController } from '../session/conversations'; +import { syncConfigurationIfNeeded } from '../session/utils/syncUtils'; // tslint:disable-next-line: no-empty-interface export interface ConversationListItemProps extends ReduxConversationType {} @@ -287,7 +287,10 @@ const ConversationListItem = (props: Props) => { * deletes the conversation */ const handleConversationDecline = async () => { - await getConversationController().deleteContact(conversationId); + // const convoToDecline = await getConversationById(conversationId); + // convoToDecline?.setIsApproved(false); + // await getConversationController().deleteContact(conversationId); // TODO: might be unnecessary + console.warn('decline'); }; /** @@ -295,12 +298,12 @@ const ConversationListItem = (props: Props) => { */ const handleConversationAccept = async () => { const conversationToApprove = await getConversationById(conversationId); - conversationToApprove?.setIsApproved(true); - console.warn('convo marked as approved'); - console.warn({ convo: conversationToApprove }); + await conversationToApprove?.setIsApproved(true); + console.warn({ convoAfterSetIsApproved: conversationToApprove }); // TODO: Send sync message to other devices. Using config message - + + await syncConfigurationIfNeeded(true); }; return ( diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index e1d57a2c1..19578b2ce 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -85,22 +85,26 @@ export class LeftPaneMessageSection extends React.Component { public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => { const { conversations } = this.props; - const approvedConversations = conversations?.filter(c => c.isApproved === true); + const approvedConversations = conversations?.filter(c => Boolean(c.isApproved) === true); if (!conversations || !approvedConversations) { throw new Error('renderRow: Tried to render without conversations'); } - // const conversation = conversations[index]; + + // TODO: make this only filtered when the setting is enabled + const messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true; + let conversation; if (approvedConversations?.length) { conversation = approvedConversations[index]; } - // TODO: need to confirm whats best here. - if ( - !conversation || - conversation?.isApproved === undefined || - conversation.isApproved === false - ) { + if (!conversation) { + return null; + } + + // TODO: need to confirm what default setting is best here. + if (messageRequestsEnabled && !Boolean(conversation.isApproved)) { return null; } return ; @@ -120,21 +124,9 @@ export class LeftPaneMessageSection extends React.Component { // TODO: make selectors for this instead. // TODO: only filter conversations if setting for requests is applied - const approvedConversations = conversations.filter(c => c.isApproved === true); - const messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true; - - const conversationsForList = messageRequestsEnabled ? approvedConversations : conversations; - if (!this.state.approvedConversations.length) { - this.setState({ - approvedConversations: conversationsForList, - }); - } - - console.warn({ conversationsForList }); - // const length = conversations.length; - const length = conversationsForList.length; + // TODO: readjust to be approved length as only approved convos will show here. + const length = this.props.conversations ? this.props.conversations.length : 0; const listKey = 0; // Note: conversations is not a known prop for List, but it is required to ensure that @@ -146,8 +138,8 @@ export class LeftPaneMessageSection extends React.Component { {({ height, width }) => ( { - await channels.updateConversation(data); + const cleanedData = _cleanData(data); + await channels.updateConversation(cleanedData); } export async function removeConversation(id: string): Promise { @@ -601,7 +602,6 @@ export async function cleanLastHashes(): Promise { await channels.cleanLastHashes(); } -// TODO: Strictly type the following export async function saveSeenMessageHashes( data: Array<{ expiresAt: number; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 78e785bde..d6ae0acc6 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -176,6 +176,7 @@ export const fillConvoAttributesWithDefaults = ( triggerNotificationsFor: 'all', // if the settings is not set in the db, this is the default isTrustedForAttachmentDownload: false, // we don't trust a contact until we say so isPinned: false, + isApproved: false, }); }; @@ -777,7 +778,6 @@ export class ConversationModel extends Backbone.Model { const chatMessagePrivate = new VisibleMessage(chatMessageParams); await getMessageQueue().sendToPubKey(destinationPubkey, chatMessagePrivate); - // this.setIsApproved(true); // consider the conversation approved even if the message fails to send return; } @@ -1389,21 +1389,12 @@ export class ConversationModel extends Backbone.Model { } public async setIsApproved(value: boolean) { - console.warn(`Setting ${this.attributes.nickname} isApproved to:: ${value}`); if (value !== this.get('isApproved')) { + console.warn(`Setting ${this.attributes.profileName} isApproved to:: ${value}`); this.set({ isApproved: value, }); await this.commit(); - - if (window?.inboxStore) { - window.inboxStore?.dispatch( - conversationChanged({ - id: this.id, - data: this.getConversationModelProps(), - }) - ); - } } } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 63590efcc..cca1743b1 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { createOrUpdateItem, getItemById, hasSyncedInitialConfigurationItem } from '../data/data'; +import { createOrUpdateItem } from '../data/data'; import { ConversationTypeEnum } from '../models/conversation'; import { joinOpenGroupV2WithUIEvents, @@ -53,14 +53,16 @@ async function handleGroupsAndContactsFromConfigMessage( envelope: EnvelopePlus, configMessage: SignalService.ConfigurationMessage ) { - const didWeHandleAConfigurationMessageAlready = - (await getItemById(hasSyncedInitialConfigurationItem))?.value || false; - if (didWeHandleAConfigurationMessageAlready) { - window?.log?.info( - 'Dropping configuration contacts/groups change as we already handled one... ' - ); - return; - } + // const didWeHandleAConfigurationMessageAlready = + // (await getItemById(hasSyncedInitialConfigurationItem))?.value || false; + + // TODO: debug + // if (didWeHandleAConfigurationMessageAlready) { + // window?.log?.info( + // 'Dropping configuration contacts/groups change as we already handled one... ' + // ); + // return; + // } await createOrUpdateItem({ id: 'hasSyncedInitialConfigurationItem', value: true, @@ -125,6 +127,7 @@ async function handleGroupsAndContactsFromConfigMessage( }; // updateProfile will do a commit for us contactConvo.set('active_at', _.toNumber(envelope.timestamp)); + contactConvo.setIsApproved(Boolean(c.isApproved)); await updateProfileOneAtATime(contactConvo, profile, c.profileKey); } catch (e) { diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index f5422bc33..aec1ebba4 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -377,6 +377,7 @@ export async function innerHandleContentMessage( } if (content.configurationMessage) { // this one can be quite long (downloads profilePictures and everything, is do not block) + console.warn('@@config message received. contentmessage.ts'); void handleConfigurationMessage( envelope, content.configurationMessage as SignalService.ConfigurationMessage diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 3e0988f87..08f3e39a0 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -99,7 +99,6 @@ export class ConversationController { const create = async () => { try { - debugger; await saveConversation(conversation.attributes); } catch (error) { window?.log?.error( diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 11eeaaad6..07f191477 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -1,5 +1,6 @@ import { createOrUpdateItem, + getAllConversations, getItemById, getLatestClosedGroupEncryptionKeyPair, } from '../../../ts/data/data'; @@ -36,16 +37,24 @@ const getLastSyncTimestampFromDb = async (): Promise => const writeLastSyncTimestampToDb = async (timestamp: number) => createOrUpdateItem({ id: ITEM_ID_LAST_SYNC_TIMESTAMP, value: timestamp }); -export const syncConfigurationIfNeeded = async () => { +/** + * Syncs usre configuration with other devices linked to this user. + * @param force Bypass duration time limit for sending sync messages + * @returns + */ +export const syncConfigurationIfNeeded = async (force: boolean = false) => { const lastSyncedTimestamp = (await getLastSyncTimestampFromDb()) || 0; const now = Date.now(); // if the last sync was less than 2 days before, return early. - if (Math.abs(now - lastSyncedTimestamp) < DURATION.DAYS * 7) { + if (!force && Math.abs(now - lastSyncedTimestamp) < DURATION.DAYS * 7) { return; } - const allConvos = getConversationController().getConversations(); + const allConvos = await (await getAllConversations()).models; + + console.warn({ test: allConvos[0].attributes.isApproved }); + // const configMessage = await getCurrentConfigurationMessage(allConvos); const configMessage = await getCurrentConfigurationMessage(allConvos); try { // window?.log?.info('syncConfigurationIfNeeded with', configMessage); @@ -191,6 +200,7 @@ const getValidContacts = (convos: Array) => { displayName: c.getLokiProfile()?.displayName, profilePictureURL: c.get('avatarPointer'), profileKey: !profileKeyForContact?.length ? undefined : profileKeyForContact, + isApproved: c.isApproved(), }); } catch (e) { window?.log.warn('getValidContacts', e); From c3924f85a9acf52718b2efed3a9ecdb90a1cd33c Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Thu, 4 Nov 2021 14:47:47 +1100 Subject: [PATCH 11/70] Adding blocking of individual requests and syncing of block to devices. Added approval by replying to a message. --- _locales/en/messages.json | 6 +- app/sql.js | 22 ++++++- protos/SignalService.proto | 3 +- ts/components/ConversationListItem.tsx | 21 ++++--- .../session/LeftPaneMessageSection.tsx | 19 +++--- .../session/MessageRequestsBanner.tsx | 10 ++-- .../session/SessionClosableOverlay.tsx | 22 +++---- ts/models/conversation.ts | 16 +++-- ts/receiver/configMessage.ts | 58 +++++++++++-------- .../conversations/ConversationController.ts | 6 ++ .../controlMessage/ConfigurationMessage.ts | 5 ++ ts/session/utils/syncUtils.ts | 3 +- ts/state/selectors/conversations.ts | 11 +++- 13 files changed, 137 insertions(+), 65 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8b649412b..ae849d3d5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -443,5 +443,9 @@ "messageDeletedPlaceholder": "This message has been deleted", "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey" + "goToOurSurvey": "Go to our survey", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceHolder": "No requests" } diff --git a/app/sql.js b/app/sql.js index 159d760cf..819cc6a4c 100644 --- a/app/sql.js +++ b/app/sql.js @@ -835,6 +835,7 @@ const LOKI_SCHEMA_VERSIONS = [ updateToLokiSchemaVersion14, updateToLokiSchemaVersion15, updateToLokiSchemaVersion16, + updateToLokiSchemaVersion17, ]; function updateToLokiSchemaVersion1(currentVersion, db) { @@ -1228,6 +1229,23 @@ function updateToLokiSchemaVersion16(currentVersion, db) { console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); } +function updateToLokiSchemaVersion17(currentVersion, db) { + const targetVersion = 17; + if (currentVersion >= targetVersion) { + return; + } + console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`); + + db.transaction(() => { + db.exec(` + ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isApproved BOOLEAN; + `); + + writeLokiSchemaVersion(targetVersion, db); + })(); + console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); +} + function writeLokiSchemaVersion(newVersion, db) { db.prepare( `INSERT INTO loki_schema( @@ -1604,8 +1622,8 @@ function updateConversation(data) { profileName, } = data; + // TODO: msgreq - remove console.log({ usrData: data }); - console.log({ usrDataTrace: console.trace() }); console.log('usrData@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'); globalInstance @@ -1619,7 +1637,7 @@ function updateConversation(data) { name = $name, isApproved = $isApproved, profileName = $profileName - WHERE id = $id;` + WHERE id = $id;` ) .run({ id, diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 0d3658963..989c2fd32 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -170,7 +170,8 @@ message ConfigurationMessage { required string name = 2; optional string profilePicture = 3; optional bytes profileKey = 4; - optional bool isApproved = 5; + optional bool isApproved = 5; + optional bool isBlocked = 6; } repeated ClosedGroup closedGroups = 1; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 6991e5ff1..6af908935 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -31,6 +31,7 @@ import { Flex } from './basic/Flex'; import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { getConversationById } from '../data/data'; import { syncConfigurationIfNeeded } from '../session/utils/syncUtils'; +import { BlockedNumberController } from '../util'; // tslint:disable-next-line: no-empty-interface export interface ConversationListItemProps extends ReduxConversationType {} @@ -284,13 +285,16 @@ const ConversationListItem = (props: Props) => { ); /** - * deletes the conversation + * Removes conversation from requests list, + * adds ID to block list, syncs the block with linked devices. */ - const handleConversationDecline = async () => { - // const convoToDecline = await getConversationById(conversationId); - // convoToDecline?.setIsApproved(false); - // await getConversationController().deleteContact(conversationId); // TODO: might be unnecessary - console.warn('decline'); + const handleConversationBlock = async () => { + const convoToBlock = await getConversationById(conversationId); + if (!convoToBlock) { + window?.log?.error('Unable to find conversation to be blocked.'); + } + await BlockedNumberController.block(convoToBlock?.id); + await syncConfigurationIfNeeded(true); }; /** @@ -302,7 +306,6 @@ const ConversationListItem = (props: Props) => { console.warn({ convoAfterSetIsApproved: conversationToApprove }); // TODO: Send sync message to other devices. Using config message - await syncConfigurationIfNeeded(true); }; @@ -364,10 +367,10 @@ const ConversationListItem = (props: Props) => { justifyContent="flex-end" > - Decline + Block { public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => { const { conversations } = this.props; - const approvedConversations = conversations?.filter(c => Boolean(c.isApproved) === true); - if (!conversations || !approvedConversations) { + const conversationsToShow = conversations?.filter(async c => { + return ( + Boolean(c.isApproved) === true && + (await BlockedNumberController.isBlockedAsync(c.id)) === false + ); + }); + if (!conversations || !conversationsToShow) { throw new Error('renderRow: Tried to render without conversations'); } @@ -95,8 +101,8 @@ export class LeftPaneMessageSection extends React.Component { window.inboxStore?.getState().userConfig.messageRequests === true; let conversation; - if (approvedConversations?.length) { - conversation = approvedConversations[index]; + if (conversationsToShow?.length) { + conversation = conversationsToShow[index]; } if (!conversation) { @@ -298,11 +304,8 @@ export class LeftPaneMessageSection extends React.Component { this.handleToggleOverlay(undefined); }} onButtonClick={async () => { - // decline all convos - // close modal - // this.state.approvedConversations.map(async(convo) => { + // TODO: msgrequest iterate all convos and block console.warn('Test'); - // } ) }} searchTerm={searchTerm} searchResults={searchResults} diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index 8f319fb8d..8766e9d09 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -83,10 +83,12 @@ export const CirclularIcon = (props: { iconType: SessionIconType; iconSize: Sess export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { const { handleOnClick } = props; - const convos = useSelector(getLeftPaneLists).conversations; - const pendingRequestsCount = (convos.filter(c => c.isApproved !== true) || []).length; + const convos = useSelector(getLeftPaneLists).conversationRequests; + // const pendingRequestsCount = ( + // convos.filter(c => Boolean(c.isApproved) === false && !Boolean(c.isBlocked)) || [] + // ).length; - if (!pendingRequestsCount) { + if (!convos.length) { return null; } @@ -95,7 +97,7 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { Message Requests -
{pendingRequestsCount}
+
{convos.length || 0}
); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 9536fead5..54414e585 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -141,10 +141,10 @@ export class SessionClosableOverlay extends React.Component { placeholder = window.i18n('createClosedGroupPlaceholder'); break; case SessionClosableOverlayType.MessageRequests: - title = 'Message Requests'; - buttonText = 'Decline All'; - subtitle = 'Pending Requests'; - placeholder = 'placeholder'; + title = window.i18n('messageRequests'); + buttonText = window.i18n('blockAll'); + subtitle = window.i18n('requestsSubtitle'); + placeholder = window.i18n('requestsPlaceholder'); break; default: } @@ -291,16 +291,18 @@ export class SessionClosableOverlay extends React.Component { } } +/** + * A request needs to be be unapproved and not blocked to be valid. + * @returns List of message request items + */ const MessageRequestList = () => { - // get all conversations with (accepted / known) const lists = useSelector(getLeftPaneLists); - const unapprovedConversations = lists?.conversations.filter(c => { - return !c.isApproved; - }) as Array; - console.warn({ unapprovedConversationsListConstructor: unapprovedConversations }); + // const validConversationRequests = lists?.conversations.filter(c => { + const validConversationRequests = lists?.conversationRequests; + console.warn({ unapprovedConversationsListConstructor: validConversationRequests }); return (
- {unapprovedConversations.map(conversation => { + {validConversationRequests.map(conversation => { return ; })}
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index d6ae0acc6..6cce5e326 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -50,6 +50,7 @@ import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsMana import { IMAGE_JPEG } from '../types/MIME'; import { UnsendMessage } from '../session/messages/outgoing/controlMessage/UnsendMessage'; import { getLatestTimestampOffset, networkDeleteMessages } from '../session/snode_api/SNodeAPI'; +import { syncConfigurationIfNeeded } from '../session/utils/syncUtils'; export enum ConversationTypeEnum { GROUP = 'group', @@ -715,9 +716,9 @@ export class ConversationModel extends Backbone.Model { const sentAt = message.get('sent_at'); - // TODO: for debugging - if (message.get('body')?.includes('unapprove')) { - console.warn('setting to unapprove'); + // TODO: msgreq for debugging + const unapprove = message.get('body')?.includes('unapprove'); + if (unapprove) { await this.setIsApproved(false); } @@ -740,6 +741,13 @@ export class ConversationModel extends Backbone.Model { lokiProfile: UserUtils.getOurProfile(), }; + const updateApprovalNeeded = + !this.isApproved() && (this.isPrivate() || this.isMediumGroup() || this.isClosedGroup()); + if (updateApprovalNeeded && !unapprove) { + this.setIsApproved(true); + await syncConfigurationIfNeeded(true); + } + if (this.isOpenGroupV2()) { const chatMessageOpenGroupV2 = new OpenGroupVisibleMessage(chatMessageParams); const roomInfos = this.toOpenGroupV2(); @@ -1506,7 +1514,7 @@ export class ConversationModel extends Backbone.Model { } public isApproved() { - return this.get('isApproved'); + return Boolean(this.get('isApproved')); } public getTitle() { diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index cca1743b1..e0b0fbb3a 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -11,6 +11,7 @@ import { getConversationController } from '../session/conversations'; import { UserUtils } from '../session/utils'; import { toHex } from '../session/utils/String'; import { configurationMessageReceived, trigger } from '../shims/events'; +import { BlockedNumberController } from '../util'; import { removeFromCache } from './cache'; import { handleNewClosedGroup } from './closedGroups'; import { updateProfileOneAtATime } from './dataMessage'; @@ -111,33 +112,42 @@ async function handleGroupsAndContactsFromConfigMessage( } } if (configMessage.contacts?.length) { - await Promise.all( - configMessage.contacts.map(async c => { - try { - if (!c.publicKey) { - return; - } - const contactConvo = await getConversationController().getOrCreateAndWait( - toHex(c.publicKey), - ConversationTypeEnum.PRIVATE - ); - const profile = { - displayName: c.name, - profilePictre: c.profilePicture, - }; - // updateProfile will do a commit for us - contactConvo.set('active_at', _.toNumber(envelope.timestamp)); - contactConvo.setIsApproved(Boolean(c.isApproved)); - - await updateProfileOneAtATime(contactConvo, profile, c.profileKey); - } catch (e) { - window?.log?.warn('failed to handle a new closed group from configuration message'); - } - }) - ); + await Promise.all(configMessage.contacts.map(async c => handleContactReceived(c, envelope))); } } +const handleContactReceived = async ( + contactReceived: SignalService.ConfigurationMessage.IContact, + envelope: EnvelopePlus +) => { + try { + if (!contactReceived.publicKey) { + return; + } + const contactConvo = await getConversationController().getOrCreateAndWait( + toHex(contactReceived.publicKey), + ConversationTypeEnum.PRIVATE + ); + const profile = { + displayName: contactReceived.name, + profilePictre: contactReceived.profilePicture, + }; + // updateProfile will do a commit for us + contactConvo.set('active_at', _.toNumber(envelope.timestamp)); + contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); + + if (contactReceived.isBlocked === true) { + await BlockedNumberController.block(contactConvo.id); + } else { + await BlockedNumberController.unblock(contactConvo.id); + } + + await updateProfileOneAtATime(contactConvo, profile, contactReceived.profileKey); + } catch (e) { + window?.log?.warn('failed to handle a new closed group from configuration message'); + } +}; + export async function handleConfigurationMessage( envelope: EnvelopePlus, configurationMessage: SignalService.ConfigurationMessage diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 08f3e39a0..ae840959b 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -262,6 +262,12 @@ export class ConversationController { return Array.from(this.conversations.models); } + public getConversationRequests(): Array { + return Array.from(this.conversations.models).filter( + conversation => conversation.isApproved() && !conversation.isBlocked + ); + } + public unsafeDelete(convo: ConversationModel) { this.conversations.remove(convo); } diff --git a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts index 89f8b710d..d6e5ee656 100644 --- a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts @@ -94,6 +94,7 @@ export class ConfigurationMessageContact { public profilePictureURL?: string; public profileKey?: Uint8Array; public isApproved?: boolean; + public isBlocked?: boolean; public constructor({ publicKey, @@ -101,18 +102,21 @@ export class ConfigurationMessageContact { profilePictureURL, profileKey, isApproved, + isBlocked, }: { publicKey: string; displayName: string; profilePictureURL?: string; profileKey?: Uint8Array; isApproved?: boolean; + isBlocked?: boolean; }) { this.publicKey = publicKey; this.displayName = displayName; this.profilePictureURL = profilePictureURL; this.profileKey = profileKey; this.isApproved = isApproved; + this.isBlocked = isBlocked; // will throw if public key is invalid PubKey.cast(publicKey); @@ -136,6 +140,7 @@ export class ConfigurationMessageContact { profilePicture: this.profilePictureURL, profileKey: this.profileKey, isApproved: this.isApproved, + isBlocked: this.isBlocked, }); } } diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 07f191477..43db448c5 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -164,7 +164,7 @@ const getValidClosedGroups = async (convos: Array) => { const getValidContacts = (convos: Array) => { // Filter contacts const contactsModels = convos.filter( - c => !!c.get('active_at') && c.getLokiProfile()?.displayName && c.isPrivate() && !c.isBlocked() + c => !!c.get('active_at') && c.getLokiProfile()?.displayName && c.isPrivate() ); const contacts = contactsModels.map(c => { @@ -201,6 +201,7 @@ const getValidContacts = (convos: Array) => { profilePictureURL: c.get('avatarPointer'), profileKey: !profileKeyForContact?.length ? undefined : profileKeyForContact, isApproved: c.isApproved(), + isBlocked: c.isBlocked(), }); } catch (e) { window?.log.warn('getValidContacts', e); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 7a5e4a580..fd2ed734e 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -275,6 +275,7 @@ export const _getLeftPaneLists = ( ): { conversations: Array; contacts: Array; + conversationRequests: Array; unreadCount: number; } => { const values = Object.values(lookup); @@ -282,6 +283,7 @@ export const _getLeftPaneLists = ( const conversations: Array = []; const directConversations: Array = []; + const conversationRequests: Array = []; let unreadCount = 0; for (let conversation of sorted) { @@ -317,6 +319,10 @@ export const _getLeftPaneLists = ( directConversations.push(conversation); } + if (!conversation.isApproved && !conversation.isBlocked) { + conversationRequests.push(conversation); + } + if ( unreadCount < 9 && conversation.unreadCount && @@ -326,12 +332,15 @@ export const _getLeftPaneLists = ( unreadCount += conversation.unreadCount; } - conversations.push(conversation); + if (conversation.isApproved) { + conversations.push(conversation); + } } return { conversations, contacts: directConversations, + conversationRequests, unreadCount, }; }; From 87235641cb14f4f34a3da9dfb59d8dc4656f56d1 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Thu, 4 Nov 2021 16:07:27 +1100 Subject: [PATCH 12/70] fixed typos for translations and method name. --- _locales/en/messages.json | 2 +- ts/receiver/queuedJob.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ae849d3d5..dba5ec8dc 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -447,5 +447,5 @@ "blockAll": "Block All", "messageRequests": "Message Requests", "requestsSubtitle": "Pending Requests", - "requestsPlaceHolder": "No requests" + "requestsPlaceholder": "No requests" } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index a1847bd8e..c0a35b5d4 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -463,7 +463,7 @@ export async function handleMessageJob( conversationKey: conversation.id, messageModelProps: message.getMessageModelProps(), }); - trotthledAllMessagesAddedDispatch(); + throttledAllMessagesAddedDispatch(); if (message.get('unread')) { await conversation.throttledNotify(message); } @@ -479,7 +479,7 @@ export async function handleMessageJob( } } -const trotthledAllMessagesAddedDispatch = _.throttle(() => { +const throttledAllMessagesAddedDispatch = _.throttle(() => { if (updatesToDispatch.size === 0) { return; } From 6a62437c3e33c5eb7f90c7fb27b6c27745e1cd0e Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Fri, 12 Nov 2021 13:29:35 +1100 Subject: [PATCH 13/70] Blocking, accepting on click and accepting on msg send working across clients. --- app/sql.js | 4 ---- ts/components/ConversationListItem.tsx | 7 ++++--- ts/components/session/LeftPaneMessageSection.tsx | 16 +++++++++++++++- ts/models/conversation.ts | 5 +++++ ts/receiver/configMessage.ts | 1 + ts/receiver/contentMessage.ts | 1 - ts/receiver/dataMessage.ts | 9 +++++++-- ts/receiver/queuedJob.ts | 3 +++ ts/receiver/receiver.ts | 9 ++++----- ts/session/utils/syncUtils.ts | 6 ++++-- 10 files changed, 43 insertions(+), 18 deletions(-) diff --git a/app/sql.js b/app/sql.js index 819cc6a4c..76ae257a7 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1622,10 +1622,6 @@ function updateConversation(data) { profileName, } = data; - // TODO: msgreq - remove - console.log({ usrData: data }); - console.log('usrData@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'); - globalInstance .prepare( `UPDATE ${CONVERSATIONS_TABLE} SET diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 6af908935..9decf0223 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -30,7 +30,7 @@ import { ConversationNotificationSettingType } from '../models/conversation'; import { Flex } from './basic/Flex'; import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { getConversationById } from '../data/data'; -import { syncConfigurationIfNeeded } from '../session/utils/syncUtils'; +import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { BlockedNumberController } from '../util'; // tslint:disable-next-line: no-empty-interface @@ -294,7 +294,7 @@ const ConversationListItem = (props: Props) => { window?.log?.error('Unable to find conversation to be blocked.'); } await BlockedNumberController.block(convoToBlock?.id); - await syncConfigurationIfNeeded(true); + await forceSyncConfigurationNowIfNeeded(); }; /** @@ -306,7 +306,8 @@ const ConversationListItem = (props: Props) => { console.warn({ convoAfterSetIsApproved: conversationToApprove }); // TODO: Send sync message to other devices. Using config message - await syncConfigurationIfNeeded(true); + // await syncConfigurationIfNeeded(true); + await forceSyncConfigurationNowIfNeeded(); }; return ( diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 9fa401bdc..68a3b818d 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -30,6 +30,7 @@ import { clearSearch, search, updateSearchTerm } from '../../state/ducks/search' import _ from 'lodash'; import { MessageRequestsBanner } from './MessageRequestsBanner'; import { BlockedNumberController } from '../../util'; +import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; export interface Props { searchTerm: string; @@ -305,7 +306,20 @@ export class LeftPaneMessageSection extends React.Component { }} onButtonClick={async () => { // TODO: msgrequest iterate all convos and block - console.warn('Test'); + // iterate all conversations and set all to approve then + const allConversations = getConversationController().getConversations(); + let syncRequired = false; + + _.forEach(allConversations, convo => { + if (convo.isApproved() !== true) { + convo.setIsApproved(true); + syncRequired = true; + } + }); + if (syncRequired) { + // syncConfigurationIfNeeded(true); + await forceSyncConfigurationNowIfNeeded(); + } }} searchTerm={searchTerm} searchResults={searchResults} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 6cce5e326..b747c2bd2 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1161,6 +1161,11 @@ export class ConversationModel extends Backbone.Model { public async addSingleMessage(messageAttributes: MessageAttributesOptionals, setToExpire = true) { const model = new MessageModel(messageAttributes); + const isMe = messageAttributes.source === UserUtils.getOurPubKeyStrFromCache(); + if (isMe) { + await this.setIsApproved(true); + } + // no need to trigger a UI update now, we trigger a messageAdded just below const messageId = await model.commit(false); model.set({ id: messageId }); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index e0b0fbb3a..a8d7f0ec6 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -152,6 +152,7 @@ export async function handleConfigurationMessage( envelope: EnvelopePlus, configurationMessage: SignalService.ConfigurationMessage ): Promise { + window?.log?.info('Handling configuration message'); const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); if (!ourPubkey) { return; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index aec1ebba4..f5422bc33 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -377,7 +377,6 @@ export async function innerHandleContentMessage( } if (content.configurationMessage) { // this one can be quite long (downloads profilePictures and everything, is do not block) - console.warn('@@config message received. contentmessage.ts'); void handleConfigurationMessage( envelope, content.configurationMessage as SignalService.ConfigurationMessage diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index aff80bf61..751ffe2db 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -32,11 +32,14 @@ export async function updateProfileOneAtATime( } const oneAtaTimeStr = `updateProfileOneAtATime:${conversation.id}`; return allowOnlyOneAtATime(oneAtaTimeStr, async () => { - return updateProfile(conversation, profile, profileKey); + return createOrUpdateProfile(conversation, profile, profileKey); }); } -async function updateProfile( +/** + * Creates a new profile from the profile provided. Creates the profile if it doesn't exist. + */ +async function createOrUpdateProfile( conversation: ConversationModel, profile: SignalService.DataMessage.ILokiProfile, profileKey?: Uint8Array | null // was any @@ -400,6 +403,7 @@ export async function isMessageDuplicate({ return false; } const filteredResult = [result].filter((m: any) => m.attributes.body === message.body); + console.warn({ filteredResult }); return filteredResult.some(m => isDuplicate(m, message, source)); } catch (error) { window?.log?.error('isMessageDuplicate error:', Errors.toLogFormat(error)); @@ -420,6 +424,7 @@ export const isDuplicate = ( Math.abs(m.attributes.sent_at - testedMessage.timestamp) <= PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES; + debugger; return sameUsername && sameText && timestampsSimilar; }; diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index c0a35b5d4..6c185b5d8 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -313,6 +313,9 @@ async function handleRegularMessage( if (type === 'outgoing') { handleSyncedReceipts(message, conversation); + + // TODO: Can we assume sync receipts are always from linked device outgoings? + if (dataMessage.body !== 'unapprove') conversation.setIsApproved(true); } const conversationActiveAt = conversation.get('active_at'); diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 601a4f1fd..6a2a342fd 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -161,12 +161,11 @@ export function handleRequest(body: any, options: ReqOptions, messageHash: strin incomingMessagePromises.push(promise); } -// tslint:enable:cyclomatic-complexity max-func-body-length */ - -// *********************************************************************** -// *********************************************************************** -// *********************************************************************** +// tslint:enable:cyclomatic-complexity max-func-body-length */ +/** + * Used in background.js + */ export async function queueAllCached() { const items = await getAllFromCache(); items.forEach(async item => { diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 43db448c5..52ca3e15a 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -70,8 +70,9 @@ export const syncConfigurationIfNeeded = async (force: boolean = false) => { }; export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = false) => - new Promise(resolve => { - const allConvos = getConversationController().getConversations(); + new Promise(async resolve => { + // const allConvos = getConversationController().getConversations(); + const allConvos = (await getAllConversations()).models; // if we hang for more than 10sec, force resolve this promise. setTimeout(() => { @@ -88,6 +89,7 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal resolve(true); } : undefined; + console.warn({configMessage}); void getMessageQueue().sendSyncMessage(configMessage, callback as any); // either we resolve from the callback if we need to wait for it, // or we don't want to wait, we resolve it here. From 690abb9d52b1b6a42927de3743c1b809d5a1c8fa Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Fri, 12 Nov 2021 13:49:19 +1100 Subject: [PATCH 14/70] adding simple PR changes requested. --- ts/components/session/MessageRequestsBanner.tsx | 3 --- ts/state/ducks/userConfig.tsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index 8766e9d09..f281bface 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -84,9 +84,6 @@ export const CirclularIcon = (props: { iconType: SessionIconType; iconSize: Sess export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { const { handleOnClick } = props; const convos = useSelector(getLeftPaneLists).conversationRequests; - // const pendingRequestsCount = ( - // convos.filter(c => Boolean(c.isApproved) === false && !Boolean(c.isBlocked)) || [] - // ).length; if (!convos.length) { return null; diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index ea34b9cfd..8628dcd20 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -30,7 +30,7 @@ const userConfigSlice = createSlice({ state.messageRequests = false; }, enableMessageRequests: state => { - state.messageRequests = true; + state.messageRequests = false; }, }, }); From cb5551c1e9cdba8548ae5d0808d55c0dcd85cc48 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Sun, 14 Nov 2021 22:38:07 +1100 Subject: [PATCH 15/70] PR changes --- app/sql.js | 21 ------------------- .../session/LeftPaneMessageSection.tsx | 3 +-- ts/models/conversation.ts | 2 +- ts/receiver/dataMessage.ts | 1 - ts/session/utils/syncUtils.ts | 1 - ts/state/selectors/conversations.ts | 16 -------------- 6 files changed, 2 insertions(+), 42 deletions(-) diff --git a/app/sql.js b/app/sql.js index 76ae257a7..f81655201 100644 --- a/app/sql.js +++ b/app/sql.js @@ -835,7 +835,6 @@ const LOKI_SCHEMA_VERSIONS = [ updateToLokiSchemaVersion14, updateToLokiSchemaVersion15, updateToLokiSchemaVersion16, - updateToLokiSchemaVersion17, ]; function updateToLokiSchemaVersion1(currentVersion, db) { @@ -1229,23 +1228,6 @@ function updateToLokiSchemaVersion16(currentVersion, db) { console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); } -function updateToLokiSchemaVersion17(currentVersion, db) { - const targetVersion = 17; - if (currentVersion >= targetVersion) { - return; - } - console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`); - - db.transaction(() => { - db.exec(` - ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isApproved BOOLEAN; - `); - - writeLokiSchemaVersion(targetVersion, db); - })(); - console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); -} - function writeLokiSchemaVersion(newVersion, db) { db.prepare( `INSERT INTO loki_schema( @@ -1618,7 +1600,6 @@ function updateConversation(data) { type, members, name, - isApproved, profileName, } = data; @@ -1631,7 +1612,6 @@ function updateConversation(data) { type = $type, members = $members, name = $name, - isApproved = $isApproved, profileName = $profileName WHERE id = $id;` ) @@ -1643,7 +1623,6 @@ function updateConversation(data) { type, members: members ? members.join(' ') : null, name, - isApproved, profileName, }); } diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 68a3b818d..6ee88f3d2 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -312,12 +312,11 @@ export class LeftPaneMessageSection extends React.Component { _.forEach(allConversations, convo => { if (convo.isApproved() !== true) { - convo.setIsApproved(true); + BlockedNumberController.block(convo.id); syncRequired = true; } }); if (syncRequired) { - // syncConfigurationIfNeeded(true); await forceSyncConfigurationNowIfNeeded(); } }} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index b747c2bd2..b7f107557 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -146,7 +146,7 @@ export interface ConversationAttributesOptionals { triggerNotificationsFor?: ConversationNotificationSettingType; isTrustedForAttachmentDownload?: boolean; isPinned: boolean; - isApproved: boolean; + isApproved?: boolean; } /** diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 751ffe2db..47a8d3d68 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -424,7 +424,6 @@ export const isDuplicate = ( Math.abs(m.attributes.sent_at - testedMessage.timestamp) <= PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES; - debugger; return sameUsername && sameText && timestampsSimilar; }; diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 52ca3e15a..9c6017bd5 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -89,7 +89,6 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal resolve(true); } : undefined; - console.warn({configMessage}); void getMessageQueue().sendSyncMessage(configMessage, callback as any); // either we resolve from the callback if we need to wait for it, // or we don't want to wait, we resolve it here. diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index fd2ed734e..15ec06bba 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -352,22 +352,6 @@ export const getLeftPaneLists = createSelector( _getLeftPaneLists ); -export const getApprovedConversations = createSelector( - getConversationLookup, - (lookup: ConversationLookupType): Array => { - return Object.values(lookup).filter(convo => convo.isApproved === true); - } -); - -export const getUnapprovedConversations = createSelector( - getConversationLookup, - (lookup: ConversationLookupType): Array => { - return Object.values(lookup).filter( - convo => convo.isApproved === false || convo.isApproved === undefined - ); - } -); - export const getMe = createSelector( [getConversationLookup, getOurNumber], (lookup: ConversationLookupType, ourNumber: string): ReduxConversationType => { From e5a203a48e837a741dde1e002d6cd82a8cf16b64 Mon Sep 17 00:00:00 2001 From: warrickct Date: Tue, 16 Nov 2021 13:09:44 +1100 Subject: [PATCH 16/70] adding setting of active_at to hide unapproved messages. --- ts/models/conversation.ts | 4 ++++ ts/receiver/queuedJob.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index b7f107557..173ad05f2 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1407,6 +1407,10 @@ export class ConversationModel extends Backbone.Model { this.set({ isApproved: value, }); + + // to exclude the conversation from left pane messages list and message requests + if (value === false) this.set({ active_at: undefined }); + await this.commit(); } } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 6c185b5d8..00ce7d73c 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -314,8 +314,8 @@ async function handleRegularMessage( if (type === 'outgoing') { handleSyncedReceipts(message, conversation); - // TODO: Can we assume sync receipts are always from linked device outgoings? - if (dataMessage.body !== 'unapprove') conversation.setIsApproved(true); + // assumes sync receipts are always from linked device outgoings? + conversation.setIsApproved(true); } const conversationActiveAt = conversation.get('active_at'); From 2eab74246bd8d5e0119aff3c17c588ba0c8b690f Mon Sep 17 00:00:00 2001 From: warrickct Date: Tue, 16 Nov 2021 13:22:12 +1100 Subject: [PATCH 17/70] PR changes. Disabling message requests behind feature flags. --- preload.js | 1 + ts/components/ConversationListItem.tsx | 8 ----- ts/components/LeftPane.tsx | 3 -- .../session/LeftPaneMessageSection.tsx | 36 ++++++------------- .../session/SessionClosableOverlay.tsx | 3 -- ts/models/conversation.ts | 8 +---- ts/receiver/configMessage.ts | 10 ------ ts/receiver/dataMessage.ts | 1 - ts/state/selectors/conversations.ts | 4 +++ ts/window.d.ts | 1 + 10 files changed, 17 insertions(+), 58 deletions(-) diff --git a/preload.js b/preload.js index 281b08ee2..9cf51698a 100644 --- a/preload.js +++ b/preload.js @@ -51,6 +51,7 @@ window.lokiFeatureFlags = { padOutgoingAttachments: true, enablePinConversations: true, useUnsendRequests: false, + useMessageRequests: false, }; window.isBeforeVersion = (toCheck, baseVersion) => { diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 9decf0223..c412b4db3 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -303,10 +303,6 @@ const ConversationListItem = (props: Props) => { const handleConversationAccept = async () => { const conversationToApprove = await getConversationById(conversationId); await conversationToApprove?.setIsApproved(true); - console.warn({ convoAfterSetIsApproved: conversationToApprove }); - // TODO: Send sync message to other devices. Using config message - - // await syncConfigurationIfNeeded(true); await forceSyncConfigurationNowIfNeeded(); }; @@ -315,10 +311,6 @@ const ConversationListItem = (props: Props) => {
{ - // e.stopPropagation(); - // e.preventDefault(); - // }} onContextMenu={(e: any) => { contextMenu.show({ id: triggerId, diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 56bfe79bc..271485155 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -30,9 +30,6 @@ const InnerLeftPaneMessageSection = () => { const lists = showSearch ? undefined : useSelector(getLeftPaneLists); // tslint:disable: use-simple-attributes - - // const - return ( { @@ -66,19 +64,12 @@ export class LeftPaneMessageSection extends React.Component { public constructor(props: Props) { super(props); - const approvedConversations = props.conversations?.filter(convo => Boolean(convo.isApproved)); - const unapprovedConversations = props.conversations?.filter( - convo => !Boolean(convo.isApproved) - ); - console.warn('convos updated'); this.state = { loading: false, overlay: false, valuePasted: '', - approvedConversations: approvedConversations || [], - unapprovedConversations: unapprovedConversations || [], }; autoBind(this); @@ -87,23 +78,19 @@ export class LeftPaneMessageSection extends React.Component { public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => { const { conversations } = this.props; - const conversationsToShow = conversations?.filter(async c => { - return ( - Boolean(c.isApproved) === true && - (await BlockedNumberController.isBlockedAsync(c.id)) === false - ); - }); - if (!conversations || !conversationsToShow) { + + //assume conversations that have been marked unapproved should be filtered out by selector. + if (!conversations) { throw new Error('renderRow: Tried to render without conversations'); } - // TODO: make this only filtered when the setting is enabled const messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true; + window.inboxStore?.getState().userConfig.messageRequests === true && + window?.lokiFeatureFlags?.useMessageRequests; let conversation; - if (conversationsToShow?.length) { - conversation = conversationsToShow[index]; + if (conversations?.length) { + conversation = conversations[index]; } if (!conversation) { @@ -129,10 +116,6 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('render: must provided conversations if no search results are provided'); } - // TODO: make selectors for this instead. - // TODO: only filter conversations if setting for requests is applied - - // TODO: readjust to be approved length as only approved convos will show here. const length = this.props.conversations ? this.props.conversations.length : 0; const listKey = 0; @@ -145,7 +128,6 @@ export class LeftPaneMessageSection extends React.Component { {({ height, width }) => ( { onChange={this.updateSearch} placeholder={window.i18n('searchFor...')} /> - + {window.lokiFeatureFlags.useMessageRequests ? ( + + ) : null} {this.renderList()} {this.renderBottomButtons()}
diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 54414e585..c5b532aa1 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -297,9 +297,7 @@ export class SessionClosableOverlay extends React.Component { */ const MessageRequestList = () => { const lists = useSelector(getLeftPaneLists); - // const validConversationRequests = lists?.conversations.filter(c => { const validConversationRequests = lists?.conversationRequests; - console.warn({ unapprovedConversationsListConstructor: validConversationRequests }); return (
{validConversationRequests.map(conversation => { @@ -309,7 +307,6 @@ const MessageRequestList = () => { ); }; -// const MessageRequestListItem = (props: { conversation: ConversationModel }) => { const MessageRequestListItem = (props: { conversation: ConversationListItemProps }) => { const { conversation } = props; return ( diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 173ad05f2..c05077662 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -716,12 +716,6 @@ export class ConversationModel extends Backbone.Model { const sentAt = message.get('sent_at'); - // TODO: msgreq for debugging - const unapprove = message.get('body')?.includes('unapprove'); - if (unapprove) { - await this.setIsApproved(false); - } - if (!sentAt) { throw new Error('sendMessageJob() sent_at must be set.'); } @@ -743,7 +737,7 @@ export class ConversationModel extends Backbone.Model { const updateApprovalNeeded = !this.isApproved() && (this.isPrivate() || this.isMediumGroup() || this.isClosedGroup()); - if (updateApprovalNeeded && !unapprove) { + if (updateApprovalNeeded) { this.setIsApproved(true); await syncConfigurationIfNeeded(true); } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index a8d7f0ec6..4df55f14d 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -54,16 +54,6 @@ async function handleGroupsAndContactsFromConfigMessage( envelope: EnvelopePlus, configMessage: SignalService.ConfigurationMessage ) { - // const didWeHandleAConfigurationMessageAlready = - // (await getItemById(hasSyncedInitialConfigurationItem))?.value || false; - - // TODO: debug - // if (didWeHandleAConfigurationMessageAlready) { - // window?.log?.info( - // 'Dropping configuration contacts/groups change as we already handled one... ' - // ); - // return; - // } await createOrUpdateItem({ id: 'hasSyncedInitialConfigurationItem', value: true, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 47a8d3d68..e6720c81b 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -403,7 +403,6 @@ export async function isMessageDuplicate({ return false; } const filteredResult = [result].filter((m: any) => m.attributes.body === message.body); - console.warn({ filteredResult }); return filteredResult.some(m => isDuplicate(m, message, source)); } catch (error) { window?.log?.error('isMessageDuplicate error:', Errors.toLogFormat(error)); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 15ec06bba..8cbed2136 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -304,6 +304,10 @@ export const _getLeftPaneLists = ( }; } + if (!Boolean(conversation.isApproved) === true && window.lokiFeatureFlags.useMessageRequests) { + continue; + } + // Add Open Group to list as soon as the name has been set if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { continue; diff --git a/ts/window.d.ts b/ts/window.d.ts index 61d1a538f..488b79281 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -48,6 +48,7 @@ declare global { padOutgoingAttachments: boolean; enablePinConversations: boolean; useUnsendRequests: boolean; + useMessageRequests: boolean; }; lokiSnodeAPI: LokiSnodeAPI; onLogin: any; From 40396224dc870a100fc9f5ef5f1581e9cdfab4ca Mon Sep 17 00:00:00 2001 From: warrickct Date: Tue, 16 Nov 2021 14:40:52 +1100 Subject: [PATCH 18/70] adding feature flag for config message receiving --- ts/receiver/configMessage.ts | 13 ++++++++----- ts/receiver/queuedJob.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 4df55f14d..37f0e2637 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -124,12 +124,15 @@ const handleContactReceived = async ( }; // updateProfile will do a commit for us contactConvo.set('active_at', _.toNumber(envelope.timestamp)); - contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); - if (contactReceived.isBlocked === true) { - await BlockedNumberController.block(contactConvo.id); - } else { - await BlockedNumberController.unblock(contactConvo.id); + if (window.lokiFeatureFlags.useMessageRequests === true) { + contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); + + if (contactReceived.isBlocked === true) { + await BlockedNumberController.block(contactConvo.id); + } else { + await BlockedNumberController.unblock(contactConvo.id); + } } await updateProfileOneAtATime(contactConvo, profile, contactReceived.profileKey); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 00ce7d73c..9be31a3a8 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -311,7 +311,7 @@ async function handleRegularMessage( updateReadStatus(message, conversation); } - if (type === 'outgoing') { + if (type === 'outgoing' && window.lokiFeatureFlags.useMessageRequests) { handleSyncedReceipts(message, conversation); // assumes sync receipts are always from linked device outgoings? From 22e4c9d850a1098fc7e4cbe12aa6681898c247fd Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 10:21:19 +1100 Subject: [PATCH 19/70] fix archlinux pw unused issue on archlinux, the appimage links to the system sqlite by default which does not support sqlcipher --- package.json | 2 +- ts/receiver/callMessage.ts | 4 +--- yarn.lock | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ffa61da21..7a4f72254 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "abort-controller": "3.0.0", "auto-bind": "^4.0.0", "backbone": "1.3.3", - "better-sqlite3": "https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006", + "better-sqlite3": "https://github.com/signalapp/better-sqlite3#ad0db5dd09c0ea4007b1c46bd4f7273827803347", "blob-util": "1.3.0", "blueimp-canvas-to-blob": "3.14.0", "blueimp-load-image": "2.18.0", diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index 47fc25600..bec8f2f6b 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -2,12 +2,10 @@ import _ from 'lodash'; import { SignalService } from '../protobuf'; import { TTL_DEFAULT } from '../session/constants'; import { SNodeAPI } from '../session/snode_api'; -import { CallManager } from '../session/utils'; +import { CallManager, UserUtils } from '../session/utils'; import { removeFromCache } from './cache'; import { EnvelopePlus } from './types'; -// audric FIXME: refactor this out to persistence, just to help debug the flow and send/receive in synchronous testing - export async function handleCallMessage( envelope: EnvelopePlus, callMessage: SignalService.CallMessage diff --git a/yarn.lock b/yarn.lock index 11f13bf5c..b66dbbada 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2180,9 +2180,9 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -"better-sqlite3@https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006": +"better-sqlite3@https://github.com/signalapp/better-sqlite3#ad0db5dd09c0ea4007b1c46bd4f7273827803347": version "7.1.4" - resolved "https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006" + resolved "https://github.com/signalapp/better-sqlite3#ad0db5dd09c0ea4007b1c46bd4f7273827803347" dependencies: bindings "^1.5.0" tar "^6.1.0" From 1d3a89f05809d7f5c1c7946b3784f3b180eb16a3 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 10:22:54 +1100 Subject: [PATCH 20/70] hide activeAt = 0 convo from search results Fixes #2033 --- ts/state/selectors/search.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index c0f60b067..e60a3cc2f 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -44,8 +44,9 @@ export const getSearchResults = createSelector( state.conversations.map(id => { const value = lookup[id]; - // Don't return anything when activeAt is undefined (i.e. no current conversations with this user) - if (value.activeAt === undefined) { + // Don't return anything when activeAt is unset (i.e. no current conversations with this user) + if (value.activeAt === undefined || value.activeAt === 0) { + //activeAt can be 0 when linking device return null; } From 465508b2ae610dabf5ae11b03523d2b2b9b3df13 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 10:37:40 +1100 Subject: [PATCH 21/70] opengroup messages from blocked user are dropped Fixes #2019 --- ts/opengroup/opengroupV2/ApiUtil.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index 56993469d..bb345ce1f 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -3,6 +3,7 @@ import { FileServerV2Request } from '../../fileserver/FileServerApiV2'; import { PubKey } from '../../session/types'; import { allowOnlyOneAtATime } from '../../session/utils/Promise'; import { updateDefaultRooms, updateDefaultRoomsInProgress } from '../../state/ducks/defaultRooms'; +import { BlockedNumberController } from '../../util'; import { getCompleteUrlFromRoom } from '../utils/OpenGroupUtils'; import { parseOpenGroupV2 } from './JoinOpenGroupV2'; import { getAllRoomInfos } from './OpenGroupAPIV2'; @@ -100,7 +101,11 @@ export const parseMessages = async ( } } - return _.compact(parsedMessages).sort((a, b) => (a.serverId || 0) - (b.serverId || 0)); + return _.compact( + parsedMessages.map(m => + m && m.sender && !BlockedNumberController.isBlocked(m.sender) ? m : null + ) + ).sort((a, b) => (a.serverId || 0) - (b.serverId || 0)); }; // tslint:disable: no-http-string const defaultServerUrl = 'http://116.203.70.33'; From 7b0587876fb5081993ac18500de0166983e70a0c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 10:37:40 +1100 Subject: [PATCH 22/70] opengroup messages from blocked user are dropped Fixes #2019 --- ts/opengroup/opengroupV2/ApiUtil.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index 56993469d..bb345ce1f 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -3,6 +3,7 @@ import { FileServerV2Request } from '../../fileserver/FileServerApiV2'; import { PubKey } from '../../session/types'; import { allowOnlyOneAtATime } from '../../session/utils/Promise'; import { updateDefaultRooms, updateDefaultRoomsInProgress } from '../../state/ducks/defaultRooms'; +import { BlockedNumberController } from '../../util'; import { getCompleteUrlFromRoom } from '../utils/OpenGroupUtils'; import { parseOpenGroupV2 } from './JoinOpenGroupV2'; import { getAllRoomInfos } from './OpenGroupAPIV2'; @@ -100,7 +101,11 @@ export const parseMessages = async ( } } - return _.compact(parsedMessages).sort((a, b) => (a.serverId || 0) - (b.serverId || 0)); + return _.compact( + parsedMessages.map(m => + m && m.sender && !BlockedNumberController.isBlocked(m.sender) ? m : null + ) + ).sort((a, b) => (a.serverId || 0) - (b.serverId || 0)); }; // tslint:disable: no-http-string const defaultServerUrl = 'http://116.203.70.33'; From c1471426acd45159f536f20ee4d157da41645229 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 16:01:33 +1100 Subject: [PATCH 23/70] dismiss a call when answered from another of our devices --- ts/receiver/callMessage.ts | 17 ++ ts/session/sending/MessageSentHandler.ts | 11 +- ts/session/snode_api/swarmPolling.ts | 2 - ts/session/utils/CallManager.ts | 195 ++++++++++++++++------- 4 files changed, 161 insertions(+), 64 deletions(-) diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index bec8f2f6b..d7ef83232 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -17,6 +17,23 @@ export async function handleCallMessage( const { type } = callMessage; + // we just allow self send of ANSWER message to remove the incoming call dialog when we accepted it from another device + if ( + sender === UserUtils.getOurPubKeyStrFromCache() && + callMessage.type !== SignalService.CallMessage.Type.ANSWER + ) { + window.log.info('Dropping incoming call from ourself'); + await removeFromCache(envelope); + return; + } + + if (CallManager.isCallRejected(callMessage.uuid)) { + await removeFromCache(envelope); + + window.log.info(`Dropping already rejected call ${callMessage.uuid}`); + return; + } + if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) { await removeFromCache(envelope); diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 666e66d57..9d78abd10 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -1,13 +1,11 @@ import _ from 'lodash'; import { getMessageById } from '../../data/data'; -import { MessageModel } from '../../models/message'; import { SignalService } from '../../protobuf'; import { PnServer } from '../../pushnotification'; import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { EncryptionType, RawMessage } from '../types'; import { UserUtils } from '../utils'; -// tslint:disable-next-line no-unnecessary-class export class MessageSentHandler { public static async handlePublicMessageSentSuccess( sentMessage: OpenGroupVisibleMessage, @@ -54,10 +52,8 @@ export class MessageSentHandler { let sentTo = fetchedMessage.get('sent_to') || []; - let isOurDevice = false; - if (sentMessage.device) { - isOurDevice = UserUtils.isUsFromCache(sentMessage.device); - } + const isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + // FIXME this is not correct and will cause issues with syncing // At this point the only way to check for medium // group is by comparing the encryption type @@ -113,8 +109,9 @@ export class MessageSentHandler { window?.log?.warn( 'Got an error while trying to sendSyncMessage(): fetchedMessage is null' ); + return; } - fetchedMessage = tempFetchMessage as MessageModel; + fetchedMessage = tempFetchMessage; } catch (e) { window?.log?.warn('Got an error while trying to sendSyncMessage():', e); } diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index 437c32333..85aab1156 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -318,7 +318,6 @@ export class SwarmPolling { } private loadGroupIds() { - // Start polling for medium size groups as well (they might be in different swarms) const convos = getConversationController().getConversations(); const mediumGroupsOnly = convos.filter( @@ -328,7 +327,6 @@ export class SwarmPolling { mediumGroupsOnly.forEach((c: any) => { this.addGroupId(new PubKey(c.id)); - // TODO: unsubscribe if the group is deleted }); } diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 56f613ed6..d639358e3 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { MessageUtils, ToastUtils } from '.'; +import { MessageUtils, ToastUtils, UserUtils } from '.'; import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; import { getConversationById } from '../../data/data'; import { ConversationModel } from '../../models/conversation'; @@ -28,6 +28,8 @@ export type InputItem = { deviceId: string; label: string }; let currentCallUUID: string | undefined; +const rejectedCallUUIDS: Set = new Set(); + export type CallManagerOptionsType = { localStream: MediaStream | null; remoteStream: MediaStream | null; @@ -80,7 +82,7 @@ export function removeVideoEventsListener(uniqueId: string) { } /** - * This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per device cache. + * This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per call cache. */ const callCache = new Map>>(); @@ -203,7 +205,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { if (!peerConnection) { throw new Error('cannot selectCameraByDeviceId without a peer connection'); } - const sender = peerConnection.getSenders().find(s => { + let sender = peerConnection.getSenders().find(s => { + return s.track?.kind === videoTrack.kind; + }); + + // video might be completely off + if (!sender) { + peerConnection.addTrack(videoTrack); + } + sender = peerConnection.getSenders().find(s => { return s.track?.kind === videoTrack.kind; }); if (sender) { @@ -217,8 +227,6 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { sendVideoStatusViaDataChannel(); callVideoListeners(); - } else { - throw new Error('Failed to get sender for selectCameraByDeviceId '); } } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); @@ -313,7 +321,7 @@ async function handleNegotiationNeededEvent(recipient: string) { uuid: currentCallUUID, }); - window.log.info('sending OFFER MESSAGE'); + window.log.info(`sending OFFER MESSAGE with callUUID: ${currentCallUUID}`); const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( PubKey.cast(recipient), offerMessage @@ -340,10 +348,7 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) { async function openMediaDevicesAndAddTracks() { try { await updateConnectedDevices(); - if (!camerasList.length) { - ToastUtils.pushNoCameraFound(); - return; - } + if (!audioInputsList.length) { ToastUtils.pushNoAudioInputFound(); return; @@ -352,34 +357,32 @@ async function openMediaDevicesAndAddTracks() { selectedAudioInputId = audioInputsList[0].deviceId; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; window.log.info( - `openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}` + `openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}` ); const devicesConfig = { audio: { - deviceId: selectedAudioInputId, + deviceId: { exact: selectedAudioInputId }, echoCancellation: true, }, - video: { - deviceId: selectedCameraId, - // width: VIDEO_WIDTH, - // height: Math.floor(VIDEO_WIDTH * VIDEO_RATIO), - }, + // we don't need a video stream on start + video: false, }; mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig); mediaDevices.getTracks().map(track => { - if (track.kind === 'video') { - track.enabled = false; - } + // if (track.kind === 'video') { + // track.enabled = false; + // } if (mediaDevices) { peerConnection?.addTrack(track, mediaDevices); } }); } catch (err) { + window.log.warn('openMediaDevices: ', err); ToastUtils.pushVideoCallPermissionNeeded(); - closeVideoCall(); + await closeVideoCall(); } callVideoListeners(); } @@ -412,6 +415,9 @@ export async function USER_callRecipient(recipient: string) { }); window.log.info('Sending preOffer message to ', ed25519Str(recipient)); + + // we do it manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess + // which is not the case for a pre offer message (the message only exists in memory) const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg); const { wrappedEnvelope } = await MessageSender.send(rawMessage); void PnServer.notifyPnServer(wrappedEnvelope, recipient); @@ -455,7 +461,9 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => { uuid: currentCallUUID, }); - window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient); + window.log.info( + `sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}` + ); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates); }, 2000); @@ -479,7 +487,7 @@ const findLastMessageTypeFromSender = (sender: string, msgType: SignalService.Ca function handleSignalingStateChangeEvent() { if (peerConnection?.signalingState === 'closed') { - closeVideoCall(); + void closeVideoCall(); } } @@ -487,14 +495,14 @@ function handleConnectionStateChanged(pubkey: string) { window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState); if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { - closeVideoCall(); + void closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); window.inboxStore?.dispatch(callConnected({ pubkey })); } } -function closeVideoCall() { +async function closeVideoCall() { window.log.info('closingVideoCall '); setIsRinging(false); if (peerConnection) { @@ -533,6 +541,18 @@ function closeVideoCall() { currentCallUUID = undefined; window.inboxStore?.dispatch(setFullScreenCall(false)); + const convos = getConversationController().getConversations(); + const callingConvos = convos.filter(convo => convo.callState !== undefined); + if (callingConvos.length > 0) { + // reset all convos callState + await Promise.all( + callingConvos.map(async m => { + m.callState = undefined; + await m.commit(); + }) + ); + } + remoteVideoStreamIsMuted = true; makingOffer = false; @@ -712,7 +732,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } // tslint:disable-next-line: function-name -export async function USER_rejectIncomingCallRequest(fromSender: string) { +export async function USER_rejectIncomingCallRequest(fromSender: string, forcedUUID?: string) { setIsRinging(false); const lastOfferMessage = findLastMessageTypeFromSender( @@ -720,32 +740,36 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) { SignalService.CallMessage.Type.OFFER ); - const lastCallUUID = lastOfferMessage?.uuid; - window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${lastCallUUID}`); - if (lastCallUUID) { + const aboutCallUUID = forcedUUID || lastOfferMessage?.uuid; + window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`); + if (aboutCallUUID) { + rejectedCallUUIDS.add(aboutCallUUID); const endCallMessage = new CallMessage({ type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), - uuid: lastCallUUID, + uuid: aboutCallUUID, }); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); // delete all msg not from that uuid only but from that sender pubkey - clearCallCacheFromPubkeyAndUUID(fromSender, lastCallUUID); + clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID); } - window.inboxStore?.dispatch( - endCall({ - pubkey: fromSender, - }) - ); + // if we got a forceUUID, it means we just to deny another user's device incoming call we are already in a call with. + if (!forcedUUID) { + window.inboxStore?.dispatch( + endCall({ + pubkey: fromSender, + }) + ); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // we just got a new offer from someone we are already in a call with - if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { - closeVideoCall(); + const convos = getConversationController().getConversations(); + const callingConvos = convos.filter(convo => convo.callState !== undefined); + if (callingConvos.length > 0) { + // we just got a new offer from someone we are already in a call with + if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { + await closeVideoCall(); + } } } } @@ -758,6 +782,7 @@ export async function USER_hangup(fromSender: string) { window.log.warn('should not be able to hangup without a currentCallUUID'); return; } else { + rejectedCallUUIDS.add(currentCallUUID); const endCallMessage = new CallMessage({ type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), @@ -773,17 +798,19 @@ export async function USER_hangup(fromSender: string) { clearCallCacheFromPubkeyAndUUID(fromSender, currentCallUUID); - closeVideoCall(); + await closeVideoCall(); } export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { window.log.info('handling callMessage END_CALL:', aboutCallUUID); if (aboutCallUUID) { + rejectedCallUUIDS.add(aboutCallUUID); + clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); if (aboutCallUUID === currentCallUUID) { - closeVideoCall(); + void closeVideoCall(); window.inboxStore?.dispatch(endCall({ pubkey: sender })); } @@ -817,9 +844,17 @@ async function buildAnswerAndSendIt(sender: string) { window.log.info('sending ANSWER MESSAGE'); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage); + await getMessageQueue().sendToPubKeyNonDurably( + UserUtils.getOurPubKeyFromCache(), + callAnswerMessage + ); } } +export function isCallRejected(uuid: string) { + return rejectedCallUUIDS.has(uuid); +} + export async function handleCallTypeOffer( sender: string, callMessage: SignalService.CallMessage, @@ -832,20 +867,22 @@ export async function handleCallTypeOffer( } window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (!getCallMediaPermissionsSettings()) { await handleMissedCall(sender, incomingOfferTimestamp, true); return; } - if (callingConvos.length > 0) { - // we just got a new offer from someone we are NOT already in a call with - if (callingConvos.length !== 1 || callingConvos[0].id !== sender) { - await handleMissedCall(sender, incomingOfferTimestamp, false); + if (currentCallUUID && currentCallUUID !== remoteCallUUID) { + // we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one) + if (callCache.get(sender)?.has(currentCallUUID)) { + // this is a missed call from the same sender (another call from another device maybe?) + // just reject it. + await USER_rejectIncomingCallRequest(sender, remoteCallUUID); return; } + await handleMissedCall(sender, incomingOfferTimestamp, false); + + return; } const readyForOffer = @@ -859,7 +896,7 @@ export async function handleCallTypeOffer( return; } - if (callingConvos.length === 1 && callingConvos[0].id === sender) { + if (remoteCallUUID === currentCallUUID && currentCallUUID) { window.log.info('Got a new offer message from our ongoing call'); isSettingRemoteAnswerPending = false; const remoteDesc = new RTCSessionDescription({ @@ -938,7 +975,48 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe return; } - window.log.info('handling callMessage ANSWER'); + // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call + // if we accepted that call already from the current device, currentCallUUID is set + if (sender === UserUtils.getOurPubKeyStrFromCache() && remoteCallUUID !== currentCallUUID) { + window.log.info(`handling callMessage ANSWER from ourself about call ${remoteCallUUID}`); + + let foundOwnerOfCallUUID: string | undefined; + for (const deviceKey of callCache.keys()) { + if (foundOwnerOfCallUUID) { + break; + } + for (const callUUIDEntry of callCache.get(deviceKey) as Map< + string, + Array + >) { + if (callUUIDEntry[0] === remoteCallUUID) { + foundOwnerOfCallUUID = deviceKey; + break; + } + } + } + + if (foundOwnerOfCallUUID) { + rejectedCallUUIDS.add(remoteCallUUID); + + const convos = getConversationController().getConversations(); + const callingConvos = convos.filter(convo => convo.callState !== undefined); + if (callingConvos.length > 0) { + // we just got a new offer from someone we are already in a call with + if (callingConvos.length === 1 && callingConvos[0].id === foundOwnerOfCallUUID) { + await closeVideoCall(); + } + } + window.inboxStore?.dispatch( + endCall({ + pubkey: foundOwnerOfCallUUID, + }) + ); + return; + } + } else { + window.log.info(`handling callMessage ANSWER from ${remoteCallUUID}`); + } pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); @@ -946,8 +1024,15 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe window.log.info('handleCallTypeAnswer without peer connection. Dropping'); return; } - window.inboxStore?.dispatch(answerCall({ pubkey: sender })); - const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] }); + window.inboxStore?.dispatch( + answerCall({ + pubkey: sender, + }) + ); + const remoteDesc = new RTCSessionDescription({ + type: 'answer', + sdp: callMessage.sdps[0], + }); // window.log?.info('Setting remote answer pending'); isSettingRemoteAnswerPending = true; From 4ce1b7813a719685f62c818a8f131a6d134ac43f Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 18 Nov 2021 11:36:14 +1100 Subject: [PATCH 24/70] add data-testid for leftpane sections and edit profile dialog --- ts/components/Avatar.tsx | 4 +- .../conversation/ConversationHeader.tsx | 4 +- .../media-gallery/MediaGallery.tsx | 2 +- ts/components/dialog/EditProfileDialog.tsx | 11 ++- .../dialog/OnionStatusPathDialog.tsx | 4 +- ts/components/session/ActionsPanel.tsx | 81 ++++++++++++------- .../session/LeftPaneSettingSection.tsx | 2 +- ts/components/session/SessionInput.tsx | 2 +- .../session/SessionMemberListItem.tsx | 2 +- ts/components/session/SessionSearchInput.tsx | 2 +- .../session/conversation/SessionRecording.tsx | 6 +- .../conversation/SessionRightPanel.tsx | 2 +- .../session/icon/SessionIconButton.tsx | 2 + .../session/settings/SessionSettings.tsx | 2 +- 14 files changed, 81 insertions(+), 45 deletions(-) diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index acc5ead9f..0c347cce8 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -23,6 +23,7 @@ type Props = { base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data memberAvatars?: Array; // this is added by usingClosedConversationDetails onAvatarClick?: () => void; + dataTestId?: string; }; const Identicon = (props: Props) => { @@ -92,7 +93,7 @@ const AvatarImage = (props: { }; const AvatarInner = (props: Props) => { - const { avatarPath, base64Data, size, memberAvatars, name } = props; + const { avatarPath, base64Data, size, memberAvatars, name, dataTestId } = props; const [imageBroken, setImageBroken] = useState(false); // contentType is not important const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', ''); @@ -122,6 +123,7 @@ const AvatarInner = (props: Props) => { props.onAvatarClick?.(); }} role="button" + data-testid={dataTestId} > {hasImage ? ( { return (
- +
@@ -145,7 +145,7 @@ const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) = }); }} > - +
); }; diff --git a/ts/components/conversation/media-gallery/MediaGallery.tsx b/ts/components/conversation/media-gallery/MediaGallery.tsx index efffb3caa..72a69aaa6 100644 --- a/ts/components/conversation/media-gallery/MediaGallery.tsx +++ b/ts/components/conversation/media-gallery/MediaGallery.tsx @@ -45,7 +45,7 @@ const Sections = (props: Props & { selectedTab: TabType }) => { const label = type === 'media' ? window.i18n('mediaEmptyState') : window.i18n('documentsEmptyState'); - return ; + return ; } return ( diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index dfd7d09f1..4c84a81ea 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -82,7 +82,7 @@ export class EditProfileDialog extends React.Component<{}, State> { : undefined; return ( -
+
{
-

+

{sessionID}

@@ -182,10 +185,10 @@ export class EditProfileDialog extends React.Component<{}, State> { {this.renderProfileHeader()}
-

{name}

+

{name}

{ this.setState({ mode: 'edit' }); }} diff --git a/ts/components/dialog/OnionStatusPathDialog.tsx b/ts/components/dialog/OnionStatusPathDialog.tsx index 00540b6fb..dee364720 100644 --- a/ts/components/dialog/OnionStatusPathDialog.tsx +++ b/ts/components/dialog/OnionStatusPathDialog.tsx @@ -150,8 +150,9 @@ export const ModalStatusLight = (props: StatusLightType) => { export const ActionPanelOnionStatusLight = (props: { isSelected: boolean; handleClick: () => void; + dataTestId?: string; }) => { - const { isSelected, handleClick } = props; + const { isSelected, handleClick, dataTestId } = props; const onionPathsCount = useSelector(getOnionPathsCount); const firstPathLength = useSelector(getFirstOnionPathLength); @@ -179,6 +180,7 @@ export const ActionPanelOnionStatusLight = (props: { glowStartDelay={0} noScale={true} isSelected={isSelected} + dataTestId={dataTestId} /> ); }; diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 4952fdffc..8e09ca7f9 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { SessionIconButton, SessionIconType } from './icon'; +import { SessionIconButton } from './icon'; import { Avatar, AvatarSize } from '../Avatar'; import { SessionToastContainer } from './SessionToastContainer'; import { getConversationController } from '../../session/conversations'; @@ -96,47 +96,71 @@ const Section = (props: { type: SectionType; avatarPath?: string | null }) => { onAvatarClick={handleClick} name={userName} pubkey={ourNumber} + dataTestId="leftpane-primary-avatar" /> ); } const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined; - let iconType: SessionIconType; switch (type) { case SectionType.Message: - iconType = 'chatBubble'; - break; + return ( + + ); case SectionType.Contact: - iconType = 'users'; - break; + return ( + + ); case SectionType.Settings: - iconType = 'gear'; - break; - case SectionType.Moon: - iconType = 'moon'; - break; + return ( + + ); + case SectionType.PathIndicator: + return ( + + ); default: - iconType = 'moon'; - } - const iconColor = undefined; - - return ( - <> - {type === SectionType.PathIndicator ? ( - - ) : ( + return ( - )} - - ); + ); + } }; const cleanUpMediasInterval = DURATION.MINUTES * 30; @@ -300,7 +324,10 @@ export const ActionsPanel = () => { -
+
diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx index 29e87d949..73ba038bf 100644 --- a/ts/components/session/LeftPaneSettingSection.tsx +++ b/ts/components/session/LeftPaneSettingSection.tsx @@ -58,7 +58,7 @@ const LeftPaneSettingsCategoryRow = (props: { item: any }) => {
{item.id === focusedSettingsSection && ( - + )}
diff --git a/ts/components/session/SessionInput.tsx b/ts/components/session/SessionInput.tsx index c870e0928..611ae6450 100644 --- a/ts/components/session/SessionInput.tsx +++ b/ts/components/session/SessionInput.tsx @@ -108,7 +108,7 @@ export class SessionInput extends React.PureComponent { return ( { this.setState({ forceShow: !this.state.forceShow, diff --git a/ts/components/session/SessionMemberListItem.tsx b/ts/components/session/SessionMemberListItem.tsx index 9557c4ebc..9cf9fd0b6 100644 --- a/ts/components/session/SessionMemberListItem.tsx +++ b/ts/components/session/SessionMemberListItem.tsx @@ -72,7 +72,7 @@ export const SessionMemberListItem = (props: Props) => { {name}
- +
); diff --git a/ts/components/session/SessionSearchInput.tsx b/ts/components/session/SessionSearchInput.tsx index 5f8512859..3c25f4301 100644 --- a/ts/components/session/SessionSearchInput.tsx +++ b/ts/components/session/SessionSearchInput.tsx @@ -21,7 +21,7 @@ export const SessionSearchInput = (props: Props) => { return (
- + onChange(e.target.value)} diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx index d8dbdf393..e98a333fd 100644 --- a/ts/components/session/conversation/SessionRecording.tsx +++ b/ts/components/session/conversation/SessionRecording.tsx @@ -125,16 +125,16 @@ export class SessionRecording extends React.Component { {isRecording && ( )} {actionPauseAudio && ( - + )} {hasRecordingAndPaused && ( - + )} {hasRecording && ( {
{ dispatch(closeRightPanel()); diff --git a/ts/components/session/icon/SessionIconButton.tsx b/ts/components/session/icon/SessionIconButton.tsx index 48271ec53..90ef8bdfb 100644 --- a/ts/components/session/icon/SessionIconButton.tsx +++ b/ts/components/session/icon/SessionIconButton.tsx @@ -10,6 +10,7 @@ interface SProps extends SessionIconProps { isSelected?: boolean; isHidden?: boolean; margin?: string; + dataTestId?: string; } const SessionIconButtonInner = React.forwardRef((props, ref) => { @@ -43,6 +44,7 @@ const SessionIconButtonInner = React.forwardRef((props, ref={ref} onClick={clickHandler} style={{ display: isHidden ? 'none' : 'flex', margin: margin ? margin : '' }} + data-testid={props.dataTestId} > {
v{window.versionInfo.version} - + {window.versionInfo.commitHash}
From 2f49228317e3bc96ddb12394f473da26c2d09809 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 09:27:52 +1100 Subject: [PATCH 25/70] update turn servers --- ts/session/utils/CallManager.ts | 34 +++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index d639358e3..6389c7197 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -105,8 +105,38 @@ const configuration: RTCConfiguration = { iceServers: [ { urls: 'turn:freyr.getsession.org', - username: 'session', - credential: 'session', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:fenrir.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:frigg.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:angus.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:hereford.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:holstein.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:brahman.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', }, ], // iceTransportPolicy: 'relay', // for now, this cause the connection to break after 30-40 sec if we enable this From 80566fd60e98856fc5cd358363a5d19c1309151b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 10:46:45 +1100 Subject: [PATCH 26/70] cleanup sessionprotobuf --- protos/SubProtocol.proto | 8 -------- 1 file changed, 8 deletions(-) diff --git a/protos/SubProtocol.proto b/protos/SubProtocol.proto index c266e53f2..a788021e5 100644 --- a/protos/SubProtocol.proto +++ b/protos/SubProtocol.proto @@ -26,13 +26,6 @@ message WebSocketRequestMessage { optional uint64 id = 4; } -message WebSocketResponseMessage { - optional uint64 id = 1; - optional uint32 status = 2; - optional string message = 3; - repeated string headers = 5; - optional bytes body = 4; -} message WebSocketMessage { enum Type { @@ -43,5 +36,4 @@ message WebSocketMessage { optional Type type = 1; optional WebSocketRequestMessage request = 2; - optional WebSocketResponseMessage response = 3; } From 6f3625f99cc516bf3039e44c20d42f4f734bb9bf Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 10:49:05 +1100 Subject: [PATCH 27/70] move the state of calling to its own slice --- .../conversation/ConversationHeader.tsx | 3 +- ts/components/session/SessionInboxView.tsx | 2 + ts/components/session/calling/CallButtons.tsx | 4 +- .../calling/CallInFullScreenContainer.tsx | 4 +- .../calling/DraggableCallContainer.tsx | 7 +- .../calling/InConversationCallContainer.tsx | 2 +- .../session/calling/IncomingCallDialog.tsx | 2 +- ts/components/session/menu/Menu.tsx | 7 +- ts/hooks/useVideoEventListener.ts | 7 +- ts/interactions/conversationInteractions.ts | 2 - ts/models/conversation.ts | 9 - ts/receiver/cache.ts | 4 +- ts/receiver/callMessage.ts | 4 +- ts/receiver/contentMessage.ts | 2 +- ts/receiver/receiver.ts | 2 +- ts/session/sending/MessageSender.ts | 7 +- ts/session/utils/CallManager.ts | 232 ++++++++++-------- ts/state/ducks/call.tsx | 111 +++++++++ ts/state/ducks/conversations.ts | 108 -------- ts/state/reducer.ts | 3 + ts/state/selectors/call.ts | 103 ++++++++ ts/state/selectors/conversations.ts | 93 ------- ts/state/smart/SessionConversation.ts | 2 +- 23 files changed, 378 insertions(+), 342 deletions(-) create mode 100644 ts/state/ducks/call.tsx create mode 100644 ts/state/selectors/call.ts diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 697b2fead..122faeeeb 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -14,8 +14,6 @@ import { getConversationHeaderProps, getConversationHeaderTitleProps, getCurrentNotificationSettingText, - getHasIncomingCall, - getHasOngoingCall, getIsSelectedNoteToSelf, getIsSelectedPrivate, getSelectedConversation, @@ -40,6 +38,7 @@ import { resetSelectedMessageIds, } from '../../state/ducks/conversations'; import { callRecipient } from '../../interactions/conversationInteractions'; +import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call'; export interface TimerOption { name: string; diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index fdb34b04d..57fb092c9 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -26,6 +26,7 @@ import { PersistGate } from 'redux-persist/integration/react'; import { persistStore } from 'redux-persist'; import { TimerOptionsArray } from '../../state/ducks/timerOptions'; import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments'; +import { initialCallState } from '../../state/ducks/call'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 @@ -105,6 +106,7 @@ export class SessionInboxView extends React.Component { timerOptions, }, stagedAttachments: getEmptyStagedAttachmentsState(), + call: initialCallState, }; this.store = createStore(initialState); diff --git a/ts/components/session/calling/CallButtons.tsx b/ts/components/session/calling/CallButtons.tsx index 0469ee810..bfb2b583b 100644 --- a/ts/components/session/calling/CallButtons.tsx +++ b/ts/components/session/calling/CallButtons.tsx @@ -1,11 +1,11 @@ import { SessionIconButton } from '../icon'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; import { InputItem } from '../../../session/utils/CallManager'; -import { setFullScreenCall } from '../../../state/ducks/conversations'; +import { setFullScreenCall } from '../../../state/ducks/call'; import { CallManager, ToastUtils } from '../../../session/utils'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { getHasOngoingCallWithPubkey } from '../../../state/selectors/conversations'; +import { getHasOngoingCallWithPubkey } from '../../../state/selectors/call'; import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton'; import styled from 'styled-components'; diff --git a/ts/components/session/calling/CallInFullScreenContainer.tsx b/ts/components/session/calling/CallInFullScreenContainer.tsx index 55d03fea6..15aa0f76a 100644 --- a/ts/components/session/calling/CallInFullScreenContainer.tsx +++ b/ts/components/session/calling/CallInFullScreenContainer.tsx @@ -4,11 +4,11 @@ import { useDispatch, useSelector } from 'react-redux'; import useKey from 'react-use/lib/useKey'; import styled from 'styled-components'; import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; -import { setFullScreenCall } from '../../../state/ducks/conversations'; +import { setFullScreenCall } from '../../../state/ducks/call'; import { getCallIsInFullScreen, getHasOngoingCallWithFocusedConvo, -} from '../../../state/selectors/conversations'; +} from '../../../state/selectors/call'; import { CallWindowControls } from './CallButtons'; import { StyledVideoElement } from './DraggableCallContainer'; diff --git a/ts/components/session/calling/DraggableCallContainer.tsx b/ts/components/session/calling/DraggableCallContainer.tsx index 1b543c5f4..7455da648 100644 --- a/ts/components/session/calling/DraggableCallContainer.tsx +++ b/ts/components/session/calling/DraggableCallContainer.tsx @@ -4,11 +4,8 @@ import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; import styled from 'styled-components'; import _ from 'underscore'; -import { - getHasOngoingCall, - getHasOngoingCallWith, - getSelectedConversationKey, -} from '../../../state/selectors/conversations'; +import { getSelectedConversationKey } from '../../../state/selectors/conversations'; +import { getHasOngoingCall, getHasOngoingCallWith } from '../../../state/selectors/call'; import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { Avatar, AvatarSize } from '../../Avatar'; import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index 2a2dc4686..d8c748ed0 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -10,7 +10,7 @@ import { getHasOngoingCallWithFocusedConvoIsOffering, getHasOngoingCallWithFocusedConvosIsConnecting, getHasOngoingCallWithPubkey, -} from '../../../state/selectors/conversations'; +} from '../../../state/selectors/call'; import { StyledVideoElement } from './DraggableCallContainer'; import { Avatar, AvatarSize } from '../../Avatar'; diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx index 5e7db9e6f..d022e2c26 100644 --- a/ts/components/session/calling/IncomingCallDialog.tsx +++ b/ts/components/session/calling/IncomingCallDialog.tsx @@ -6,7 +6,7 @@ import _ from 'underscore'; import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector'; import { ed25519Str } from '../../../session/onions/onionPath'; import { CallManager } from '../../../session/utils'; -import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/conversations'; +import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call'; import { Avatar, AvatarSize } from '../../Avatar'; import { SessionButton, SessionButtonColor } from '../SessionButton'; import { SessionWrapperModal } from '../SessionWrapperModal'; diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 5195b77cf..f325086d9 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,10 +1,7 @@ import React from 'react'; -import { - getHasIncomingCall, - getHasOngoingCall, - getNumberOfPinnedConversations, -} from '../../../state/selectors/conversations'; +import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call'; +import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; import { getFocusedSection } from '../../../state/selectors/section'; import { Item, Submenu } from 'react-contexify'; import { diff --git a/ts/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts index 203614a8e..401420cde 100644 --- a/ts/hooks/useVideoEventListener.ts +++ b/ts/hooks/useVideoEventListener.ts @@ -8,11 +8,8 @@ import { DEVICE_DISABLED_DEVICE_ID, InputItem, } from '../session/utils/CallManager'; -import { - getCallIsInFullScreen, - getHasOngoingCallWithPubkey, - getSelectedConversationKey, -} from '../state/selectors/conversations'; +import { getSelectedConversationKey } from '../state/selectors/conversations'; +import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call'; export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { const selectedConversationKey = useSelector(getSelectedConversationKey); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 16615e1dd..4851f6dc4 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -447,8 +447,6 @@ export async function callRecipient(pubkey: string, canCall: boolean) { } if (convo && convo.isPrivate() && !convo.isMe()) { - convo.callState = 'offering'; - await convo.commit(); await CallManager.USER_callRecipient(convo.id); } } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 6af47dc74..f70fe9f26 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -176,8 +176,6 @@ export const fillConvoAttributesWithDefaults = ( }); }; -export type CallState = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; - export class ConversationModel extends Backbone.Model { public updateLastMessage: () => any; public throttledBumpTyping: () => void; @@ -185,8 +183,6 @@ export class ConversationModel extends Backbone.Model { public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise; public initialPromise: any; - public callState: CallState; - private typingRefreshTimer?: NodeJS.Timeout | null; private typingPauseTimer?: NodeJS.Timeout | null; private typingTimer?: NodeJS.Timeout | null; @@ -441,7 +437,6 @@ export class ConversationModel extends Backbone.Model { const left = !!this.get('left'); const expireTimer = this.get('expireTimer'); const currentNotificationSetting = this.get('triggerNotificationsFor'); - const callState = this.callState; // to reduce the redux store size, only set fields which cannot be undefined // for instance, a boolean can usually be not set if false, etc @@ -546,10 +541,6 @@ export class ConversationModel extends Backbone.Model { text: lastMessageText, }; } - - if (callState) { - toRet.callState = callState; - } return toRet; } diff --git a/ts/receiver/cache.ts b/ts/receiver/cache.ts index a98a79c5b..d6919273a 100644 --- a/ts/receiver/cache.ts +++ b/ts/receiver/cache.ts @@ -15,7 +15,7 @@ import { export async function removeFromCache(envelope: EnvelopePlus) { const { id } = envelope; - window?.log?.info(`removing from cache envelope: ${id}`); + // window?.log?.info(`removing from cache envelope: ${id}`); return removeUnprocessed(id); } @@ -25,7 +25,7 @@ export async function addToCache( messageHash: string ) { const { id } = envelope; - window?.log?.info(`adding to cache envelope: ${id}`); + // window?.log?.info(`adding to cache envelope: ${id}`); const encodedEnvelope = StringUtils.decode(plaintext, 'base64'); const data: UnprocessedParameter = { diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index d7ef83232..8b0d4122e 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -30,7 +30,7 @@ export async function handleCallMessage( if (CallManager.isCallRejected(callMessage.uuid)) { await removeFromCache(envelope); - window.log.info(`Dropping already rejected call ${callMessage.uuid}`); + window.log.info(`Dropping already rejected call from this device ${callMessage.uuid}`); return; } @@ -65,7 +65,7 @@ export async function handleCallMessage( if (type === SignalService.CallMessage.Type.END_CALL) { await removeFromCache(envelope); - CallManager.handleCallTypeEndCall(sender, callMessage.uuid); + await CallManager.handleCallTypeEndCall(sender, callMessage.uuid); return; } diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 7e160ca5e..09d4236fa 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -210,7 +210,7 @@ async function decryptUnidentifiedSender( envelope: EnvelopePlus, ciphertext: ArrayBuffer ): Promise { - window?.log?.info('received unidentified sender message'); + // window?.log?.info('received unidentified sender message'); try { const userX25519KeyPair = await UserUtils.getIdentityKeyPair(); diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index c3a8c0521..b35827ca5 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -72,7 +72,7 @@ const envelopeQueue = new EnvelopeQueue(); function queueEnvelope(envelope: EnvelopePlus, messageHash?: string) { const id = getEnvelopeId(envelope); - window?.log?.info('queueing envelope', id); + // window?.log?.info('queueing envelope', id); const task = handleEnvelope.bind(null, envelope, messageHash); const taskWithTimeout = createTaskWithTimeout(task, `queueEnvelope ${id}`); diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index dde8630de..4cb519130 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -20,6 +20,7 @@ import { MessageSender } from '.'; import { getMessageById } from '../../../ts/data/data'; import { SNodeAPI } from '../snode_api'; import { getConversationController } from '../conversations'; +import { ed25519Str } from '../onions/onionPath'; const DEFAULT_CONNECTIONS = 1; @@ -129,7 +130,7 @@ export async function TEST_sendMessageToSnode( const data64 = window.dcodeIO.ByteBuffer.wrap(data).toString('base64'); const swarm = await getSwarmFor(pubKey); - window?.log?.debug('Sending envelope with timestamp: ', timestamp, ' to ', pubKey); + window?.log?.debug('Sending envelope with timestamp: ', timestamp, ' to ', ed25519Str(pubKey)); // send parameters const params = { pubKey, @@ -190,7 +191,9 @@ export async function TEST_sendMessageToSnode( } window?.log?.info( - `loki_message:::sendMessage - Successfully stored message to ${pubKey} via ${snode.ip}:${snode.port}` + `loki_message:::sendMessage - Successfully stored message to ${ed25519Str(pubKey)} via ${ + snode.ip + }:${snode.port}` ); } diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 6389c7197..3ab4e7866 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -2,18 +2,18 @@ import _ from 'lodash'; import { MessageUtils, ToastUtils, UserUtils } from '.'; import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; import { getConversationById } from '../../data/data'; -import { ConversationModel } from '../../models/conversation'; import { MessageModelType } from '../../models/messageType'; import { SignalService } from '../../protobuf'; +import { openConversationWithMessages } from '../../state/ducks/conversations'; import { answerCall, callConnected, + CallStatusEnum, endCall, incomingCall, - openConversationWithMessages, setFullScreenCall, startingCallWith, -} from '../../state/ducks/conversations'; +} from '../../state/ducks/call'; import { getConversationController } from '../conversations'; import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; import { ed25519Str } from '../onions/onionPath'; @@ -26,6 +26,9 @@ import { setIsRinging } from './RingingManager'; export type InputItem = { deviceId: string; label: string }; +/** + * This uuid is set only once we accepted a call or started one. + */ let currentCallUUID: string | undefined; const rejectedCallUUIDS: Set = new Set(); @@ -571,17 +574,7 @@ async function closeVideoCall() { currentCallUUID = undefined; window.inboxStore?.dispatch(setFullScreenCall(false)); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // reset all convos callState - await Promise.all( - callingConvos.map(async m => { - m.callState = undefined; - await m.commit(); - }) - ); - } + window.inboxStore?.dispatch(endCall()); remoteVideoStreamIsMuted = true; @@ -592,24 +585,26 @@ async function closeVideoCall() { callVideoListeners(); } +function getCallingStateOutsideOfRedux() { + const ongoingCallWith = window.inboxStore?.getState().call.ongoingWith as string | undefined; + const ongoingCallStatus = window.inboxStore?.getState().call.ongoingCallStatus as CallStatusEnum; + return { ongoingCallWith, ongoingCallStatus }; +} + function onDataChannelReceivedMessage(ev: MessageEvent) { try { const parsed = JSON.parse(ev.data); if (parsed.hangup !== undefined) { - const foundEntry = getConversationController() - .getConversations() - .find( - (convo: ConversationModel) => - convo.callState === 'connecting' || - convo.callState === 'offering' || - convo.callState === 'ongoing' - ); - - if (!foundEntry || !foundEntry.id) { - return; + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); + if ( + (ongoingCallStatus === 'connecting' || + ongoingCallStatus === 'offering' || + ongoingCallStatus === 'ongoing') && + ongoingCallWith + ) { + void handleCallTypeEndCall(ongoingCallWith, currentCallUUID); } - handleCallTypeEndCall(foundEntry.id, currentCallUUID); return; } @@ -761,8 +756,23 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { await buildAnswerAndSendIt(fromSender); } +export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUID: string) { + setIsRinging(false); + window.log.info(`rejectCallAlreadyAnotherCall ${ed25519Str(fromSender)}: ${forcedUUID}`); + rejectedCallUUIDS.add(forcedUUID); + const rejectCallMessage = new CallMessage({ + type: SignalService.CallMessage.Type.END_CALL, + timestamp: Date.now(), + uuid: forcedUUID, + }); + await sendCallMessageAndSync(rejectCallMessage, fromSender); + + // delete all msg not from that uuid only but from that sender pubkey + clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID); +} + // tslint:disable-next-line: function-name -export async function USER_rejectIncomingCallRequest(fromSender: string, forcedUUID?: string) { +export async function USER_rejectIncomingCallRequest(fromSender: string) { setIsRinging(false); const lastOfferMessage = findLastMessageTypeFromSender( @@ -770,7 +780,7 @@ export async function USER_rejectIncomingCallRequest(fromSender: string, forcedU SignalService.CallMessage.Type.OFFER ); - const aboutCallUUID = forcedUUID || lastOfferMessage?.uuid; + const aboutCallUUID = lastOfferMessage?.uuid; window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`); if (aboutCallUUID) { rejectedCallUUIDS.add(aboutCallUUID); @@ -779,29 +789,25 @@ export async function USER_rejectIncomingCallRequest(fromSender: string, forcedU timestamp: Date.now(), uuid: aboutCallUUID, }); - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); - + // sync the reject event so our other devices remove the popup too + await sendCallMessageAndSync(endCallMessage, fromSender); // delete all msg not from that uuid only but from that sender pubkey clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID); } + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); - // if we got a forceUUID, it means we just to deny another user's device incoming call we are already in a call with. - if (!forcedUUID) { - window.inboxStore?.dispatch( - endCall({ - pubkey: fromSender, - }) - ); - - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // we just got a new offer from someone we are already in a call with - if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { - await closeVideoCall(); - } - } + // clear the ongoing call if needed + if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) { + await closeVideoCall(); } + + // close the popup call + window.inboxStore?.dispatch(endCall()); +} + +async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { + await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage); + await getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage); } // tslint:disable-next-line: function-name @@ -821,7 +827,7 @@ export async function USER_hangup(fromSender: string) { void getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); } - window.inboxStore?.dispatch(endCall({ pubkey: fromSender })); + window.inboxStore?.dispatch(endCall()); window.log.info('sending hangup with an END_CALL MESSAGE'); sendHangupViaDataChannel(); @@ -831,7 +837,10 @@ export async function USER_hangup(fromSender: string) { await closeVideoCall(); } -export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { +/** + * This can actually be called from either the datachannel or from the receiver END_CALL event + */ +export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { window.log.info('handling callMessage END_CALL:', aboutCallUUID); if (aboutCallUUID) { @@ -839,10 +848,25 @@ export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); - if (aboutCallUUID === currentCallUUID) { - void closeVideoCall(); + // this is a end call from ourself. We must remove the popup about the incoming call + // if it matches the owner of this callUUID + if (sender === UserUtils.getOurPubKeyStrFromCache()) { + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); + const ownerOfCall = getOwnerOfCallUUID(aboutCallUUID); - window.inboxStore?.dispatch(endCall({ pubkey: sender })); + if ( + (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') && + ongoingCallWith === ownerOfCall + ) { + await closeVideoCall(); + window.inboxStore?.dispatch(endCall()); + } + return; + } + + if (aboutCallUUID === currentCallUUID) { + await closeVideoCall(); + window.inboxStore?.dispatch(endCall()); } } } @@ -871,13 +895,8 @@ async function buildAnswerAndSendIt(sender: string) { uuid: currentCallUUID, }); - window.log.info('sending ANSWER MESSAGE'); - - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage); - await getMessageQueue().sendToPubKeyNonDurably( - UserUtils.getOurPubKeyFromCache(), - callAnswerMessage - ); + window.log.info('sending ANSWER MESSAGE and sync'); + await sendCallMessageAndSync(callAnswerMessage, sender); } } @@ -905,12 +924,17 @@ export async function handleCallTypeOffer( if (currentCallUUID && currentCallUUID !== remoteCallUUID) { // we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one) if (callCache.get(sender)?.has(currentCallUUID)) { - // this is a missed call from the same sender (another call from another device maybe?) - // just reject it. - await USER_rejectIncomingCallRequest(sender, remoteCallUUID); + // this is a missed call from the same sender but with a different callID. + // another call from another device maybe? just reject it. + await rejectCallAlreadyAnotherCall(sender, remoteCallUUID); return; } + // add a message in the convo with this user about the missed call. await handleMissedCall(sender, incomingOfferTimestamp, false); + // Here, we are in a call, and we got an offer from someone we are in a call with, and not one of his other devices. + // Just hangup automatically the call on the calling side. + + await rejectCallAlreadyAnotherCall(sender, remoteCallUUID); return; } @@ -994,61 +1018,69 @@ export async function handleMissedCall( return; } +function getOwnerOfCallUUID(callUUID: string) { + for (const deviceKey of callCache.keys()) { + for (const callUUIDEntry of callCache.get(deviceKey) as Map< + string, + Array + >) { + if (callUUIDEntry[0] === callUUID) { + return deviceKey; + } + } + } + return null; +} + export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) { if (!callMessage.sdps || callMessage.sdps.length === 0) { - window.log.warn('cannot handle answered message without signal description protols'); + window.log.warn('cannot handle answered message without signal description proto sdps'); return; } - const remoteCallUUID = callMessage.uuid; - if (!remoteCallUUID || remoteCallUUID.length === 0) { + const callMessageUUID = callMessage.uuid; + if (!callMessageUUID || callMessageUUID.length === 0) { window.log.warn('handleCallTypeAnswer has no valid uuid'); return; } - // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call - // if we accepted that call already from the current device, currentCallUUID is set - if (sender === UserUtils.getOurPubKeyStrFromCache() && remoteCallUUID !== currentCallUUID) { - window.log.info(`handling callMessage ANSWER from ourself about call ${remoteCallUUID}`); + // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call. + // if we accepted that call already from the current device, currentCallUUID would be set + if (sender === UserUtils.getOurPubKeyStrFromCache()) { + // when we answer a call, we get this message on all our devices, including the one we just accepted the call with. - let foundOwnerOfCallUUID: string | undefined; - for (const deviceKey of callCache.keys()) { - if (foundOwnerOfCallUUID) { - break; - } - for (const callUUIDEntry of callCache.get(deviceKey) as Map< - string, - Array - >) { - if (callUUIDEntry[0] === remoteCallUUID) { - foundOwnerOfCallUUID = deviceKey; - break; - } - } + const isDeviceWhichJustAcceptedCall = currentCallUUID === callMessageUUID; + + if (isDeviceWhichJustAcceptedCall) { + window.log.info( + `isDeviceWhichJustAcceptedCall: skipping message back ANSWER from ourself about call ${callMessageUUID}` + ); + + return; } + window.log.info(`handling callMessage ANSWER from ourself about call ${callMessageUUID}`); - if (foundOwnerOfCallUUID) { - rejectedCallUUIDS.add(remoteCallUUID); + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); + const foundOwnerOfCallUUID = getOwnerOfCallUUID(callMessageUUID); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // we just got a new offer from someone we are already in a call with - if (callingConvos.length === 1 && callingConvos[0].id === foundOwnerOfCallUUID) { + if (callMessageUUID !== currentCallUUID) { + // this is an answer we sent from another of our devices + // automatically close that call + if (foundOwnerOfCallUUID) { + rejectedCallUUIDS.add(callMessageUUID); + // if this call is about the one being currently displayed, force close it + if (ongoingCallStatus && ongoingCallWith === foundOwnerOfCallUUID) { await closeVideoCall(); } + + window.inboxStore?.dispatch(endCall()); } - window.inboxStore?.dispatch( - endCall({ - pubkey: foundOwnerOfCallUUID, - }) - ); - return; } + return; } else { - window.log.info(`handling callMessage ANSWER from ${remoteCallUUID}`); + window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`); } - pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); + pushCallMessageToCallCache(sender, callMessageUUID, callMessage); if (!peerConnection) { window.log.info('handleCallTypeAnswer without peer connection. Dropping'); @@ -1066,7 +1098,11 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe // window.log?.info('Setting remote answer pending'); isSettingRemoteAnswerPending = true; - await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed + try { + await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed + } catch (e) { + window.log.warn('setRemoteDescription failed:', e); + } isSettingRemoteAnswerPending = false; } diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx new file mode 100644 index 000000000..47e9e3bdb --- /dev/null +++ b/ts/state/ducks/call.tsx @@ -0,0 +1,111 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export type CallStatusEnum = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; + +export type CallStateType = { + ongoingWith?: string; + ongoingCallStatus?: CallStatusEnum; + callIsInFullScreen: boolean; +}; + +export const initialCallState: CallStateType = { + ongoingWith: undefined, + ongoingCallStatus: undefined, + callIsInFullScreen: false, +}; + +/** + * This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server. + */ +const callSlice = createSlice({ + name: 'call', + initialState: initialCallState, + reducers: { + incomingCall(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + if (state.ongoingWith && state.ongoingWith !== callerPubkey) { + window.log.warn( + `Got an incoming call action for ${callerPubkey} but we are already in a call.` + ); + return state; + } + state.ongoingWith = callerPubkey; + state.ongoingCallStatus = 'incoming'; + return state; + }, + endCall(state: CallStateType) { + state.ongoingCallStatus = undefined; + state.ongoingWith = undefined; + + return state; + }, + answerCall(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + + // to answer a call we need an incoming call form that specific pubkey + + if (state.ongoingWith !== callerPubkey || state.ongoingCallStatus !== 'incoming') { + window.log.info('cannot answer a call we are not displaying a dialog with'); + return state; + } + state.ongoingCallStatus = 'connecting'; + state.callIsInFullScreen = false; + return state; + }, + callConnected(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + if (callerPubkey !== state.ongoingWith) { + window.log.info('cannot answer a call we did not start or receive first'); + return state; + } + const existingCallState = state.ongoingCallStatus; + + if (existingCallState !== 'connecting' && existingCallState !== 'offering') { + window.log.info( + 'cannot answer a call we are not connecting (and so answered) to or offering a call' + ); + return state; + } + + state.ongoingCallStatus = 'ongoing'; + state.callIsInFullScreen = false; + return state; + }, + startingCallWith(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + if (state.ongoingWith) { + window.log.warn('cannot start a call with an ongoing call already: ongoingWith'); + return state; + } + if (state.ongoingCallStatus) { + window.log.warn('cannot start a call with an ongoing call already: ongoingCallStatus'); + return state; + } + + const callerPubkey = action.payload.pubkey; + state.ongoingWith = callerPubkey; + state.ongoingCallStatus = 'offering'; + state.callIsInFullScreen = false; + + return state; + }, + setFullScreenCall(state: CallStateType, action: PayloadAction) { + // only set in full screen if we have an ongoing call + if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) { + state.callIsInFullScreen = true; + } + state.callIsInFullScreen = false; + return state; + }, + }, +}); + +const { actions, reducer } = callSlice; +export const { + incomingCall, + endCall, + answerCall, + callConnected, + startingCallWith, + setFullScreenCall, +} = actions; +export const callReducer = reducer; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index bc00e05fe..177ca87bc 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -3,7 +3,6 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getConversationController } from '../../session/conversations'; import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data'; import { - CallState, ConversationNotificationSettingType, ConversationTypeEnum, } from '../../models/conversation'; @@ -253,7 +252,6 @@ export interface ReduxConversationType { currentNotificationSetting?: ConversationNotificationSettingType; isPinned?: boolean; - callState?: CallState; } export interface NotificationForConvoOption { @@ -277,7 +275,6 @@ export type ConversationsStateType = { quotedMessage?: ReplyingToMessageProps; areMoreMessagesBeingFetched: boolean; haveDoneFirstScroll: boolean; - callIsInFullScreen: boolean; showScrollButton: boolean; animateQuotedMessageId?: string; @@ -372,7 +369,6 @@ export function getEmptyConversationState(): ConversationsStateType { mentionMembers: [], firstUnreadMessageId: undefined, haveDoneFirstScroll: false, - callIsInFullScreen: false, }; } @@ -698,7 +694,6 @@ const conversationsSlice = createSlice({ return { conversationLookup: state.conversationLookup, - callIsInFullScreen: state.callIsInFullScreen, selectedConversation: action.payload.id, areMoreMessagesBeingFetched: false, @@ -762,102 +757,6 @@ const conversationsSlice = createSlice({ state.mentionMembers = action.payload; return state; }, - incomingCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (existingCallState !== undefined) { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = 'incoming'; - - void foundConvo.commit(); - return state; - }, - endCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (!existingCallState) { - return state; - } - - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = undefined; - - void foundConvo.commit(); - return state; - }, - answerCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (!existingCallState || existingCallState !== 'incoming') { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - - foundConvo.callState = 'connecting'; - void foundConvo.commit(); - return state; - }, - callConnected(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (!existingCallState || existingCallState === 'ongoing') { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = 'ongoing'; - void foundConvo.commit(); - return state; - }, - startingCallWith(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (existingCallState) { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = 'offering'; - void foundConvo.commit(); - return state; - }, - setFullScreenCall(state: ConversationsStateType, action: PayloadAction) { - state.callIsInFullScreen = action.payload; - return state; - }, }, extraReducers: (builder: any) => { // Add reducers for additional action types here, and handle loading state as needed @@ -917,13 +816,6 @@ export const { quotedMessageToAnimate, setNextMessageToPlayId, updateMentionsMembers, - // calls - incomingCall, - endCall, - answerCall, - callConnected, - startingCallWith, - setFullScreenCall, } = actions; export async function openConversationWithMessages(args: { diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 04b206e3b..4e81828d7 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -6,6 +6,7 @@ import { reducer as user, UserStateType } from './ducks/user'; import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as section, SectionStateType } from './ducks/section'; import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms'; +import { callReducer as call, CallStateType } from './ducks/call'; import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion'; import { modalReducer as modals, ModalState } from './ducks/modalDialog'; @@ -28,6 +29,7 @@ export type StateType = { userConfig: UserConfigState; timerOptions: TimerOptionsState; stagedAttachments: StagedAttachmentsStateType; + call: CallStateType; }; export const reducers = { @@ -42,6 +44,7 @@ export const reducers = { userConfig, timerOptions, stagedAttachments, + call, }; // Making this work would require that our reducer signature supported AnyAction, not diff --git a/ts/state/selectors/call.ts b/ts/state/selectors/call.ts new file mode 100644 index 000000000..819938245 --- /dev/null +++ b/ts/state/selectors/call.ts @@ -0,0 +1,103 @@ +import { createSelector } from 'reselect'; +import { CallStateType } from '../ducks/call'; +import { ConversationsStateType, ReduxConversationType } from '../ducks/conversations'; +import { StateType } from '../reducer'; +import { getConversations, getSelectedConversationKey } from './conversations'; + +export const getCallState = (state: StateType): CallStateType => state.call; + +// --- INCOMING CALLS +export const getHasIncomingCallFrom = createSelector(getCallState, (state: CallStateType): + | string + | undefined => { + return state.ongoingWith && state.ongoingCallStatus === 'incoming' + ? state.ongoingWith + : undefined; +}); + +export const getHasIncomingCall = createSelector( + getHasIncomingCallFrom, + (withConvo: string | undefined): boolean => !!withConvo +); + +// --- ONGOING CALLS +export const getHasOngoingCallWith = createSelector( + getConversations, + getCallState, + (convos: ConversationsStateType, callState: CallStateType): ReduxConversationType | undefined => { + if ( + callState.ongoingWith && + (callState.ongoingCallStatus === 'connecting' || + callState.ongoingCallStatus === 'offering' || + callState.ongoingCallStatus === 'ongoing') + ) { + return convos.conversationLookup[callState.ongoingWith] || undefined; + } + return undefined; + } +); + +export const getHasOngoingCall = createSelector( + getHasOngoingCallWith, + (withConvo: ReduxConversationType | undefined): boolean => !!withConvo +); + +export const getHasOngoingCallWithPubkey = createSelector( + getHasOngoingCallWith, + (withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id +); + +export const getHasOngoingCallWithFocusedConvo = createSelector( + getHasOngoingCallWithPubkey, + getSelectedConversationKey, + (withPubkey, selectedPubkey) => { + return withPubkey && withPubkey === selectedPubkey; + } +); + +export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector( + getCallState, + getSelectedConversationKey, + (callState: CallStateType, selectedConvoPubkey?: string): boolean => { + if ( + !selectedConvoPubkey || + !callState.ongoingWith || + callState.ongoingCallStatus !== 'offering' || + selectedConvoPubkey !== callState.ongoingWith + ) { + return false; + } + + return true; + } +); + +export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector( + getCallState, + getSelectedConversationKey, + (callState: CallStateType, selectedConvoPubkey?: string): boolean => { + if ( + !selectedConvoPubkey || + !callState.ongoingWith || + callState.ongoingCallStatus !== 'connecting' || + selectedConvoPubkey !== callState.ongoingWith + ) { + return false; + } + + return true; + } +); + +export const getHasOngoingCallWithNonFocusedConvo = createSelector( + getHasOngoingCallWithPubkey, + getSelectedConversationKey, + (withPubkey, selectedPubkey) => { + return withPubkey && withPubkey !== selectedPubkey; + } +); + +export const getCallIsInFullScreen = createSelector( + getCallState, + (callState): boolean => callState.callIsInFullScreen +); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 301ae73d4..de9fc35a4 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -95,99 +95,6 @@ export const getConversationById = createSelector( } ); -export const getHasIncomingCallFrom = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => { - const foundEntry = Object.entries(state.conversationLookup).find( - ([_convoKey, convo]) => convo.callState === 'incoming' - ); - - if (!foundEntry) { - return undefined; - } - return foundEntry[1].id; - } -); - -export const getHasOngoingCallWith = createSelector( - getConversations, - (state: ConversationsStateType): ReduxConversationType | undefined => { - const foundEntry = Object.entries(state.conversationLookup).find( - ([_convoKey, convo]) => - convo.callState === 'connecting' || - convo.callState === 'offering' || - convo.callState === 'ongoing' - ); - - if (!foundEntry) { - return undefined; - } - return foundEntry[1]; - } -); - -export const getHasIncomingCall = createSelector( - getHasIncomingCallFrom, - (withConvo: string | undefined): boolean => !!withConvo -); - -export const getHasOngoingCall = createSelector( - getHasOngoingCallWith, - (withConvo: ReduxConversationType | undefined): boolean => !!withConvo -); - -export const getHasOngoingCallWithPubkey = createSelector( - getHasOngoingCallWith, - (withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id -); - -export const getHasOngoingCallWithFocusedConvo = createSelector( - getHasOngoingCallWithPubkey, - getSelectedConversationKey, - (withPubkey, selectedPubkey) => { - return withPubkey && withPubkey === selectedPubkey; - } -); - -export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector( - getConversations, - getSelectedConversationKey, - (state: ConversationsStateType, selectedConvoPubkey?: string): boolean => { - if (!selectedConvoPubkey) { - return false; - } - const isOffering = state.conversationLookup[selectedConvoPubkey]?.callState === 'offering'; - - return Boolean(isOffering); - } -); - -export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector( - getConversations, - getSelectedConversationKey, - (state: ConversationsStateType, selectedConvoPubkey?: string): boolean => { - if (!selectedConvoPubkey) { - return false; - } - const isOffering = state.conversationLookup[selectedConvoPubkey]?.callState === 'connecting'; - - return Boolean(isOffering); - } -); - -export const getHasOngoingCallWithNonFocusedConvo = createSelector( - getHasOngoingCallWithPubkey, - getSelectedConversationKey, - (withPubkey, selectedPubkey) => { - return withPubkey && withPubkey !== selectedPubkey; - } -); - -export const getCallIsInFullScreen = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.callIsInFullScreen -); - export const getIsTypingEnabled = createSelector( getConversations, getSelectedConversationKey, diff --git a/ts/state/smart/SessionConversation.ts b/ts/state/smart/SessionConversation.ts index 03cc046ff..0cf347fa9 100644 --- a/ts/state/smart/SessionConversation.ts +++ b/ts/state/smart/SessionConversation.ts @@ -4,7 +4,6 @@ import { SessionConversation } from '../../components/session/conversation/Sessi import { StateType } from '../reducer'; import { getTheme } from '../selectors/theme'; import { - getHasOngoingCallWithFocusedConvo, getLightBoxOptions, getSelectedConversation, getSelectedConversationKey, @@ -15,6 +14,7 @@ import { } from '../selectors/conversations'; import { getOurNumber } from '../selectors/user'; import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments'; +import { getHasOngoingCallWithFocusedConvo } from '../selectors/call'; const mapStateToProps = (state: StateType) => { return { From 1dff310820971230a89376be4b08fe8b6a58b9a1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 12:08:51 +1100 Subject: [PATCH 28/70] no video track by default and will be turn ON if asked to --- ts/receiver/callMessage.ts | 3 +- ts/session/sending/MessageSentHandler.ts | 1 + ts/session/utils/CallManager.ts | 108 +++++++++++++---------- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index 8b0d4122e..cbf0d4891 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -20,7 +20,8 @@ export async function handleCallMessage( // we just allow self send of ANSWER message to remove the incoming call dialog when we accepted it from another device if ( sender === UserUtils.getOurPubKeyStrFromCache() && - callMessage.type !== SignalService.CallMessage.Type.ANSWER + callMessage.type !== SignalService.CallMessage.Type.ANSWER && + callMessage.type !== SignalService.CallMessage.Type.END_CALL ) { window.log.info('Dropping incoming call from ourself'); await removeFromCache(envelope); diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 9d78abd10..a0c3dcc18 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -6,6 +6,7 @@ import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/Ope import { EncryptionType, RawMessage } from '../types'; import { UserUtils } from '../utils'; +// tslint:disable-next-line: no-unnecessary-class export class MessageSentHandler { public static async handlePublicMessageSentSuccess( sentMessage: OpenGroupVisibleMessage, diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 3ab4e7866..5d1171865 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -52,7 +52,7 @@ function callVideoListeners() { if (videoEventsListeners.length) { videoEventsListeners.forEach(item => { item.listener?.({ - localStream: mediaDevices, + localStream, remoteStream, camerasList, audioInputsList, @@ -92,7 +92,7 @@ const callCache = new Map>> let peerConnection: RTCPeerConnection | null; let dataChannel: RTCDataChannel | null; let remoteStream: MediaStream | null; -let mediaDevices: MediaStream | null; +let localStream: MediaStream | null; let remoteVideoStreamIsMuted = true; export const DEVICE_DISABLED_DEVICE_ID = 'off'; @@ -238,29 +238,32 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { if (!peerConnection) { throw new Error('cannot selectCameraByDeviceId without a peer connection'); } - let sender = peerConnection.getSenders().find(s => { - return s.track?.kind === videoTrack.kind; - }); - // video might be completely off - if (!sender) { - peerConnection.addTrack(videoTrack); - } - sender = peerConnection.getSenders().find(s => { + // video might be completely off on start. adding a track like this triggers a negotationneeded event + window.log.info('adding/replacing video track'); + const sender = peerConnection.getSenders().find(s => { return s.track?.kind === videoTrack.kind; }); + + videoTrack.enabled = true; if (sender) { + // this should not trigger a negotationneeded event + // and it is needed for when the video cam was never turn on await sender.replaceTrack(videoTrack); - videoTrack.enabled = true; - mediaDevices?.getVideoTracks().forEach(t => { - t.stop(); - mediaDevices?.removeTrack(t); - }); - mediaDevices?.addTrack(videoTrack); - - sendVideoStatusViaDataChannel(); - callVideoListeners(); + } else { + // this will trigger a negotiationeeded event + peerConnection.addTrack(videoTrack, newVideoStream); } + + // do the same changes locally + localStream?.getVideoTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(videoTrack); + + sendVideoStatusViaDataChannel(); + callVideoListeners(); } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); callVideoListeners(); @@ -403,19 +406,16 @@ async function openMediaDevicesAndAddTracks() { video: false, }; - mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig); - mediaDevices.getTracks().map(track => { - // if (track.kind === 'video') { - // track.enabled = false; - // } - if (mediaDevices) { - peerConnection?.addTrack(track, mediaDevices); + localStream = await navigator.mediaDevices.getUserMedia(devicesConfig); + localStream.getTracks().map(track => { + if (localStream) { + peerConnection?.addTrack(track, localStream); } }); } catch (err) { window.log.warn('openMediaDevices: ', err); ToastUtils.pushVideoCallPermissionNeeded(); - await closeVideoCall(); + closeVideoCall(); } callVideoListeners(); } @@ -520,7 +520,7 @@ const findLastMessageTypeFromSender = (sender: string, msgType: SignalService.Ca function handleSignalingStateChangeEvent() { if (peerConnection?.signalingState === 'closed') { - void closeVideoCall(); + closeVideoCall(); } } @@ -528,14 +528,14 @@ function handleConnectionStateChanged(pubkey: string) { window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState); if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { - void closeVideoCall(); + closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); window.inboxStore?.dispatch(callConnected({ pubkey })); } } -async function closeVideoCall() { +function closeVideoCall() { window.log.info('closingVideoCall '); setIsRinging(false); if (peerConnection) { @@ -551,14 +551,16 @@ async function closeVideoCall() { dataChannel.close(); dataChannel = null; } - if (mediaDevices) { - mediaDevices.getTracks().forEach(track => { + if (localStream) { + localStream.getTracks().forEach(track => { track.stop(); + localStream?.removeTrack(track); }); } if (remoteStream) { remoteStream.getTracks().forEach(track => { + track.stop(); remoteStream?.removeTrack(track); }); } @@ -567,7 +569,7 @@ async function closeVideoCall() { peerConnection = null; } - mediaDevices = null; + localStream = null; remoteStream = null; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; @@ -577,6 +579,7 @@ async function closeVideoCall() { window.inboxStore?.dispatch(endCall()); remoteVideoStreamIsMuted = true; + timestampAcceptedCall = undefined; makingOffer = false; ignoreOffer = false; @@ -638,11 +641,17 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) dataChannel.onmessage = onDataChannelReceivedMessage; dataChannel.onopen = onDataChannelOnOpen; - if (!isAcceptingCall) { - peerConnection.onnegotiationneeded = async () => { + peerConnection.onnegotiationneeded = async () => { + const shouldTriggerAnotherNeg = + isAcceptingCall && timestampAcceptedCall && Date.now() - timestampAcceptedCall > 1000; + if (!isAcceptingCall || shouldTriggerAnotherNeg) { await handleNegotiationNeededEvent(withPubkey); - }; - } + } else { + window.log.info( + 'should negotaite again but we accepted the call recently, so swallowing this one' + ); + } + }; peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; @@ -685,6 +694,8 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) return peerConnection; } +let timestampAcceptedCall: number | undefined; + // tslint:disable-next-line: function-name export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); @@ -719,6 +730,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } currentCallUUID = lastOfferMessage.uuid; + timestampAcceptedCall = Date.now(); peerConnection = createOrGetPeerConnection(fromSender, true); await openMediaDevicesAndAddTracks(); @@ -774,7 +786,8 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI // tslint:disable-next-line: function-name export async function USER_rejectIncomingCallRequest(fromSender: string) { setIsRinging(false); - + // close the popup call + window.inboxStore?.dispatch(endCall()); const lastOfferMessage = findLastMessageTypeFromSender( fromSender, SignalService.CallMessage.Type.OFFER @@ -798,16 +811,15 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) { // clear the ongoing call if needed if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) { - await closeVideoCall(); + closeVideoCall(); } - - // close the popup call - window.inboxStore?.dispatch(endCall()); } async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage); - await getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage); + await Promise.all([ + getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage), + getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage), + ]); } // tslint:disable-next-line: function-name @@ -834,7 +846,7 @@ export async function USER_hangup(fromSender: string) { clearCallCacheFromPubkeyAndUUID(fromSender, currentCallUUID); - await closeVideoCall(); + closeVideoCall(); } /** @@ -858,14 +870,14 @@ export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: stri (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') && ongoingCallWith === ownerOfCall ) { - await closeVideoCall(); + closeVideoCall(); window.inboxStore?.dispatch(endCall()); } return; } if (aboutCallUUID === currentCallUUID) { - await closeVideoCall(); + closeVideoCall(); window.inboxStore?.dispatch(endCall()); } } @@ -1069,7 +1081,7 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe rejectedCallUUIDS.add(callMessageUUID); // if this call is about the one being currently displayed, force close it if (ongoingCallStatus && ongoingCallWith === foundOwnerOfCallUUID) { - await closeVideoCall(); + closeVideoCall(); } window.inboxStore?.dispatch(endCall()); From 2e2941ba9b7b977041a07a322d230860ec3cb5ae Mon Sep 17 00:00:00 2001 From: warrickct Date: Mon, 22 Nov 2021 12:08:48 +1100 Subject: [PATCH 29/70] message request refactoring. --- preload.js | 2 +- .../session/LeftPaneMessageSection.tsx | 8 -- .../conversations/ConversationController.ts | 6 +- ts/state/selectors/conversations.ts | 132 +++++++++++++++--- 4 files changed, 115 insertions(+), 33 deletions(-) diff --git a/preload.js b/preload.js index 9cf51698a..aab289d73 100644 --- a/preload.js +++ b/preload.js @@ -51,7 +51,7 @@ window.lokiFeatureFlags = { padOutgoingAttachments: true, enablePinConversations: true, useUnsendRequests: false, - useMessageRequests: false, + useMessageRequests: true, }; window.isBeforeVersion = (toCheck, baseVersion) => { diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index c90ab435b..b78208892 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -84,10 +84,6 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('renderRow: Tried to render without conversations'); } - const messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true && - window?.lokiFeatureFlags?.useMessageRequests; - let conversation; if (conversations?.length) { conversation = conversations[index]; @@ -97,10 +93,6 @@ export class LeftPaneMessageSection extends React.Component { return null; } - // TODO: need to confirm what default setting is best here. - if (messageRequestsEnabled && !Boolean(conversation.isApproved)) { - return null; - } return ; }; diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index ae840959b..61016796b 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -236,7 +236,11 @@ export class ConversationController { if (conversation.isPrivate()) { window.log.info(`deleteContact isPrivate, marking as inactive: ${id}`); - conversation.set('active_at', undefined); + // conversation.set('active_at', undefined); + conversation.set({ + active_at: undefined, + isApproved: false, + }); await conversation.commit(); } else { window.log.info(`deleteContact !isPrivate, removing convo from DB: ${id}`); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 8cbed2136..bf9f20e52 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -304,41 +304,49 @@ export const _getLeftPaneLists = ( }; } - if (!Boolean(conversation.isApproved) === true && window.lokiFeatureFlags.useMessageRequests) { - continue; - } + const messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true && + window?.lokiFeatureFlags?.useMessageRequests === true; + + // if (!Boolean(conversation.isApproved) === true && window.lokiFeatureFlags.useMessageRequests) { + // continue; + // } // Add Open Group to list as soon as the name has been set - if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { - continue; + // if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { + // continue; + // } + + // if (!conversation.isApproved && !conversation.isBlocked) { + // conversationRequests.push(conversation); + // } + + if (shouldShowInRequestList(conversation, messageRequestsEnabled)) { + conversationRequests.push(conversation); } // Remove all invalid conversations and conversatons of devices associated // with cancelled attempted links - if (!conversation.isPublic && !conversation.activeAt) { - continue; - } + // if (!conversation.isPublic && !conversation.activeAt) { + // continue; + // } - if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { - directConversations.push(conversation); - } - - if (!conversation.isApproved && !conversation.isBlocked) { - conversationRequests.push(conversation); - } + // if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { + // directConversations.push(conversation); + // } - if ( - unreadCount < 9 && - conversation.unreadCount && - conversation.unreadCount > 0 && - conversation.currentNotificationSetting !== 'disabled' - ) { - unreadCount += conversation.unreadCount; + if (shouldShowInContacts(conversation)) { + directConversations.push(conversation); } - if (conversation.isApproved) { + if (shouldShowInConversationList(conversation, messageRequestsEnabled)) { conversations.push(conversation); + unreadCount = calculateNewUnreadTotal(unreadCount, conversation); } + + // if (conversation.isApproved) { + // conversations.push(conversation); + // } } return { @@ -349,6 +357,84 @@ export const _getLeftPaneLists = ( }; }; +const calculateNewUnreadTotal = (unreadCount: number, conversation: ReduxConversationType) => { + if ( + unreadCount < 9 && + conversation.unreadCount && + conversation.unreadCount > 0 && + conversation.currentNotificationSetting !== 'disabled' + ) { + unreadCount += conversation.unreadCount; + } + return unreadCount; +}; + +const shouldShowInRequestList = ( + conversation: ReduxConversationType, + messageRequestsEnabled: boolean +) => { + if (conversation.isPublic || conversation.isBlocked || Boolean(conversation.activeAt) === false) { + return false; + } + + if (messageRequestsEnabled) { + if (Boolean(conversation.isApproved || !conversation.isBlocked)) { + return false; + } + } + return true; +}; + +const shouldShowInContacts = (conversation: ReduxConversationType) => { + // Add Open Group to list as soon as the name has been set + if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { + return false; + } + + // if (shouldShowInRequestList(conversation)) { + // conversationRequests.push(conversation); + // } + + // Remove all invalid conversations and conversatons of devices associated + // with cancelled attempted links + if (!conversation.isPublic && !conversation.activeAt) { + return false; + } + + if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { + // directConversations.push(conversation); + return true; + } else { + return false; + } +}; + +const shouldShowInConversationList = ( + conversation: ReduxConversationType, + messageRequestsEnabled: boolean +) => { + // // Add Open Group to list as soon as the name has been set + if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { + return false; + } + + // Remove all invalid conversations and conversatons of devices associated + // with cancelled attempted links + if (!conversation.isPublic && !conversation.activeAt) { + return false; + } + + // if (!conversation.activeAt) { + // return false; + // } + + if (messageRequestsEnabled && !conversation.isApproved) { + return false; + } + + return true; +}; + export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, From d5f6180ae64293211f46a003d7852005c038bc7d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 14:36:02 +1100 Subject: [PATCH 30/70] create offer and answer ourselves and do not use the negotiation needed event. this event is causing us to loop in negotiation needed when each side try to create one, gets the answer and so on... --- ts/session/utils/CallManager.ts | 167 ++++++++++++++++---------------- ts/state/ducks/call.tsx | 1 + 2 files changed, 87 insertions(+), 81 deletions(-) diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 5d1171865..2a2f3c1c0 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -25,7 +25,10 @@ import { PnServer } from '../../pushnotification'; import { setIsRinging } from './RingingManager'; export type InputItem = { deviceId: string; label: string }; +// tslint:disable: function-name +const maxWidth = 1920; +const maxHeight = 1080; /** * This uuid is set only once we accepted a call or started one. */ @@ -166,6 +169,28 @@ if (typeof navigator !== 'undefined') { }); } +const silence = () => { + const ctx = new AudioContext(); + const oscillator = ctx.createOscillator(); + const dst = oscillator.connect(ctx.createMediaStreamDestination()); + oscillator.start(); + return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false }); +}; + +const black = () => { + const canvas = Object.assign(document.createElement('canvas'), { + width: maxWidth, + height: maxHeight, + }); + canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight); + const stream = (canvas as any).captureStream(); + return Object.assign(stream.getVideoTracks()[0], { enabled: false }); +}; + +const getBlackSilenceMediaStream = () => { + return new MediaStream([black(), silence()]); +}; + async function updateConnectedDevices() { // Get the set of cameras connected const videoCameras = await getConnectedDevices('videoinput'); @@ -219,6 +244,14 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { if (sender?.track) { sender.track.enabled = false; } + + // do the same changes locally + localStream?.getVideoTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(getBlackSilenceMediaStream().getVideoTracks()[0]); + sendVideoStatusViaDataChannel(); callVideoListeners(); return; @@ -235,24 +268,23 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { try { const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig); const videoTrack = newVideoStream.getVideoTracks()[0]; + if (!peerConnection) { throw new Error('cannot selectCameraByDeviceId without a peer connection'); } - // video might be completely off on start. adding a track like this triggers a negotationneeded event - window.log.info('adding/replacing video track'); - const sender = peerConnection.getSenders().find(s => { - return s.track?.kind === videoTrack.kind; - }); + window.log.info('replacing video track'); + const videoSender = peerConnection + .getTransceivers() + .find(t => t.sender.track?.kind === 'video')?.sender; videoTrack.enabled = true; - if (sender) { - // this should not trigger a negotationneeded event - // and it is needed for when the video cam was never turn on - await sender.replaceTrack(videoTrack); + if (videoSender) { + await videoSender.replaceTrack(videoTrack); } else { - // this will trigger a negotiationeeded event - peerConnection.addTrack(videoTrack, newVideoStream); + throw new Error( + 'We should always have a videoSender as we are using a black video when no camera are in use' + ); } // do the same changes locally @@ -266,6 +298,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { callVideoListeners(); } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); + ToastUtils.pushToastError('selectCamera', e.message); callVideoListeners(); } } @@ -281,6 +314,12 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { if (sender?.track) { sender.track.enabled = false; } + // do the same changes locally + localStream?.getAudioTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(getBlackSilenceMediaStream().getAudioTracks()[0]); callVideoListeners(); return; } @@ -295,6 +334,7 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { try { const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig); + const audioTrack = newAudioStream.getAudioTracks()[0]; if (!peerConnection) { throw new Error('cannot selectAudioInputByDeviceId without a peer connection'); @@ -331,18 +371,15 @@ export async function selectAudioOutputByDeviceId(audioOutputDeviceId: string) { } } -async function handleNegotiationNeededEvent(recipient: string) { +async function createOfferAndSendIt(recipient: string) { try { makingOffer = true; - window.log.info('got handleNegotiationNeeded event. creating offer'); - const offer = await peerConnection?.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); + window.log.info('got createOfferAndSendIt event. creating offer'); + await (peerConnection as any)?.setLocalDescription(); + const offer = peerConnection?.localDescription; if (!offer) { throw new Error('Could not create an offer'); } - await peerConnection?.setLocalDescription(offer); if (!currentCallUUID) { window.log.warn('cannot send offer without a currentCallUUID'); @@ -357,18 +394,18 @@ async function handleNegotiationNeededEvent(recipient: string) { uuid: currentCallUUID, }); - window.log.info(`sending OFFER MESSAGE with callUUID: ${currentCallUUID}`); - const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( + window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`); + const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( PubKey.cast(recipient), offerMessage ); - if (typeof negotationOfferSendResult === 'number') { + if (typeof negotiationOfferSendResult === 'number') { // window.log?.warn('setting last sent timestamp'); - lastOutgoingOfferTimestamp = negotationOfferSendResult; + lastOutgoingOfferTimestamp = negotiationOfferSendResult; } } } catch (err) { - window.log?.error(`Error on handling negotiation needed ${err}`); + window.log?.error(`Error createOfferAndSendIt ${err}`); } finally { makingOffer = false; } @@ -390,23 +427,13 @@ async function openMediaDevicesAndAddTracks() { return; } - selectedAudioInputId = audioInputsList[0].deviceId; + selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; //audioInputsList[0].deviceId; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; window.log.info( `openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}` ); - const devicesConfig = { - audio: { - deviceId: { exact: selectedAudioInputId }, - - echoCancellation: true, - }, - // we don't need a video stream on start - video: false, - }; - - localStream = await navigator.mediaDevices.getUserMedia(devicesConfig); + localStream = getBlackSilenceMediaStream(); localStream.getTracks().map(track => { if (localStream) { peerConnection?.addTrack(track, localStream); @@ -420,7 +447,6 @@ async function openMediaDevicesAndAddTracks() { callVideoListeners(); } -// tslint:disable-next-line: function-name export async function USER_callRecipient(recipient: string) { if (!getCallMediaPermissionsSettings()) { ToastUtils.pushVideoCallPermissionNeeded(); @@ -457,6 +483,7 @@ export async function USER_callRecipient(recipient: string) { await openMediaDevicesAndAddTracks(); setIsRinging(true); + await createOfferAndSendIt(recipient); } const iceCandidates: Array = new Array(); @@ -579,7 +606,6 @@ function closeVideoCall() { window.inboxStore?.dispatch(endCall()); remoteVideoStreamIsMuted = true; - timestampAcceptedCall = undefined; makingOffer = false; ignoreOffer = false; @@ -626,7 +652,7 @@ function onDataChannelOnOpen() { sendVideoStatusViaDataChannel(); } -function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) { +function createOrGetPeerConnection(withPubkey: string) { if (peerConnection) { return peerConnection; } @@ -640,21 +666,7 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) dataChannel.onmessage = onDataChannelReceivedMessage; dataChannel.onopen = onDataChannelOnOpen; - - peerConnection.onnegotiationneeded = async () => { - const shouldTriggerAnotherNeg = - isAcceptingCall && timestampAcceptedCall && Date.now() - timestampAcceptedCall > 1000; - if (!isAcceptingCall || shouldTriggerAnotherNeg) { - await handleNegotiationNeededEvent(withPubkey); - } else { - window.log.info( - 'should negotaite again but we accepted the call recently, so swallowing this one' - ); - } - }; - peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; - peerConnection.ontrack = event => { event.track.onunmute = () => { remoteStream?.addTrack(event.track); @@ -694,9 +706,6 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) return peerConnection; } -let timestampAcceptedCall: number | undefined; - -// tslint:disable-next-line: function-name export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); setIsRinging(false); @@ -730,8 +739,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } currentCallUUID = lastOfferMessage.uuid; - timestampAcceptedCall = Date.now(); - peerConnection = createOrGetPeerConnection(fromSender, true); + peerConnection = createOrGetPeerConnection(fromSender); await openMediaDevicesAndAddTracks(); @@ -783,7 +791,6 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID); } -// tslint:disable-next-line: function-name export async function USER_rejectIncomingCallRequest(fromSender: string) { setIsRinging(false); // close the popup call @@ -822,7 +829,6 @@ async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { ]); } -// tslint:disable-next-line: function-name export async function USER_hangup(fromSender: string) { window.log.info('USER_hangup'); @@ -889,16 +895,12 @@ async function buildAnswerAndSendIt(sender: string) { window.log.warn('cannot send answer without a currentCallUUID'); return; } - - const answer = await peerConnection.createAnswer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); + await (peerConnection as any).setLocalDescription(); + const answer = peerConnection.localDescription; if (!answer?.sdp || answer.sdp.length === 0) { window.log.warn('failed to create answer'); return; } - await peerConnection.setLocalDescription(answer); const answerSdp = answer.sdp; const callAnswerMessage = new CallMessage({ timestamp: Date.now(), @@ -955,25 +957,26 @@ export async function handleCallTypeOffer( !makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending); const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp; const offerCollision = !readyForOffer; - ignoreOffer = !polite && offerCollision; + if (ignoreOffer) { window.log?.warn('Received offer when unready for offer; Ignoring offer.'); return; } - if (remoteCallUUID === currentCallUUID && currentCallUUID) { + if (peerConnection && remoteCallUUID === currentCallUUID && currentCallUUID) { window.log.info('Got a new offer message from our ongoing call'); - isSettingRemoteAnswerPending = false; - const remoteDesc = new RTCSessionDescription({ + + const remoteOfferDesc = new RTCSessionDescription({ type: 'offer', sdp: callMessage.sdps[0], }); isSettingRemoteAnswerPending = false; - if (peerConnection) { - await peerConnection.setRemoteDescription(remoteDesc); // SRD rolls back as needed - await buildAnswerAndSendIt(sender); - } + + await peerConnection.setRemoteDescription(remoteOfferDesc); // SRD rolls back as needed + isSettingRemoteAnswerPending = false; + + await buildAnswerAndSendIt(sender); } else { window.inboxStore?.dispatch(incomingCall({ pubkey: sender })); @@ -1103,19 +1106,21 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe pubkey: sender, }) ); - const remoteDesc = new RTCSessionDescription({ - type: 'answer', - sdp: callMessage.sdps[0], - }); - // window.log?.info('Setting remote answer pending'); - isSettingRemoteAnswerPending = true; try { + isSettingRemoteAnswerPending = true; + + const remoteDesc = new RTCSessionDescription({ + type: 'answer', + sdp: callMessage.sdps[0], + }); + await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed } catch (e) { - window.log.warn('setRemoteDescription failed:', e); + window.log.warn('setRemoteDescriptio failed:', e); + } finally { + isSettingRemoteAnswerPending = false; } - isSettingRemoteAnswerPending = false; } export async function handleCallTypeIceCandidates( diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx index 47e9e3bdb..7f0c55a27 100644 --- a/ts/state/ducks/call.tsx +++ b/ts/state/ducks/call.tsx @@ -92,6 +92,7 @@ const callSlice = createSlice({ // only set in full screen if we have an ongoing call if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) { state.callIsInFullScreen = true; + return state; } state.callIsInFullScreen = false; return state; From 53289298a9eba01ffa27462ec8b51e15b340e0db Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 14:39:38 +1100 Subject: [PATCH 31/70] auto select the first audio input on connection success webrtc --- ts/session/utils/CallManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 2a2f3c1c0..d4d068950 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -558,6 +558,10 @@ function handleConnectionStateChanged(pubkey: string) { closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); + const firstAudioInput = audioInputsList?.[0].deviceId || undefined; + if (firstAudioInput) { + void selectAudioInputByDeviceId(firstAudioInput); + } window.inboxStore?.dispatch(callConnected({ pubkey })); } } From a4daabfa752c3efe74dbb31779dde80c44510535 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 15:04:43 +1100 Subject: [PATCH 32/70] add a way to choose the audioouput/mute a webrtc call --- ts/components/session/calling/CallButtons.tsx | 68 +++++++++--------- .../calling/InConversationCallContainer.tsx | 8 +-- ts/hooks/useVideoEventListener.ts | 9 +-- ts/session/utils/{ => calling}/CallManager.ts | 69 ++++++++----------- ts/session/utils/calling/Silence.ts | 24 +++++++ ts/session/utils/index.ts | 2 +- 6 files changed, 95 insertions(+), 85 deletions(-) rename ts/session/utils/{ => calling}/CallManager.ts (95%) create mode 100644 ts/session/utils/calling/Silence.ts diff --git a/ts/components/session/calling/CallButtons.tsx b/ts/components/session/calling/CallButtons.tsx index bfb2b583b..8b14869dd 100644 --- a/ts/components/session/calling/CallButtons.tsx +++ b/ts/components/session/calling/CallButtons.tsx @@ -1,6 +1,6 @@ import { SessionIconButton } from '../icon'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; -import { InputItem } from '../../../session/utils/CallManager'; +import { InputItem } from '../../../session/utils/calling/CallManager'; import { setFullScreenCall } from '../../../state/ducks/call'; import { CallManager, ToastUtils } from '../../../session/utils'; import React from 'react'; @@ -71,16 +71,16 @@ export const AudioInputButton = ({ export const AudioOutputButton = ({ currentConnectedAudioOutputs, -}: // isAudioOutputMuted, -// hideArrowIcon = false, -{ + isAudioOutputMuted, + hideArrowIcon = false, +}: { currentConnectedAudioOutputs: Array; isAudioOutputMuted: boolean; hideArrowIcon?: boolean; }) => { return ( <> - {/* { @@ -90,7 +90,7 @@ export const AudioOutputButton = ({ showAudioOutputMenu(currentConnectedAudioOutputs, e); }} hidePopoverArrow={hideArrowIcon} - /> */} + /> , -// e: React.MouseEvent -// ) => { -// if (currentConnectedAudioOutputs.length === 0) { -// ToastUtils.pushNoAudioOutputFound(); -// return; -// } -// contextMenu.show({ -// id: audioOutputTriggerId, -// event: e, -// }); -// }; +const showAudioOutputMenu = ( + currentConnectedAudioOutputs: Array, + e: React.MouseEvent +) => { + if (currentConnectedAudioOutputs.length === 0) { + ToastUtils.pushNoAudioOutputFound(); + return; + } + contextMenu.show({ + id: audioOutputTriggerId, + event: e, + }); +}; const showVideoInputMenu = ( currentConnectedCameras: Array, @@ -300,22 +300,22 @@ const handleMicrophoneToggle = async ( } }; -// const handleSpeakerToggle = async ( -// currentConnectedAudioOutputs: Array, -// isAudioOutputMuted: boolean -// ) => { -// if (!currentConnectedAudioOutputs.length) { -// ToastUtils.pushNoAudioInputFound(); +const handleSpeakerToggle = async ( + currentConnectedAudioOutputs: Array, + isAudioOutputMuted: boolean +) => { + if (!currentConnectedAudioOutputs.length) { + ToastUtils.pushNoAudioInputFound(); -// return; -// } -// if (isAudioOutputMuted) { -// // selects the first one -// await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId); -// } else { -// await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID); -// } -// }; + return; + } + if (isAudioOutputMuted) { + // selects the first one + await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId); + } else { + await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID); + } +}; const StyledCallWindowControls = styled.div` position: absolute; diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index d8c748ed0..f7d42944f 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -23,7 +23,7 @@ import { import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots'; import { CallWindowControls } from './CallButtons'; import { SessionSpinner } from '../SessionSpinner'; -import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/CallManager'; +import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/calling/CallManager'; const VideoContainer = styled.div` height: 100%; @@ -156,10 +156,10 @@ export const InConversationCallContainer = () => { if (videoRefRemote.current) { if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) { - videoRefLocal.current.muted = true; + videoRefRemote.current.muted = true; } else { - // void videoRefLocal.current.setSinkId(currentSelectedAudioOutput); - videoRefLocal.current.muted = false; + // void videoRefRemote.current.setSinkId(currentSelectedAudioOutput); + videoRefRemote.current.muted = false; } } } diff --git a/ts/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts index 401420cde..515be62d7 100644 --- a/ts/hooks/useVideoEventListener.ts +++ b/ts/hooks/useVideoEventListener.ts @@ -2,12 +2,13 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; // tslint:disable-next-line: no-submodule-imports import useMountedState from 'react-use/lib/useMountedState'; -import { CallManager } from '../session/utils'; import { + addVideoEventsListener, CallManagerOptionsType, DEVICE_DISABLED_DEVICE_ID, InputItem, -} from '../session/utils/CallManager'; + removeVideoEventsListener, +} from '../session/utils/calling/CallManager'; import { getSelectedConversationKey } from '../state/selectors/conversations'; import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call'; @@ -40,7 +41,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { (onSame && ongoingCallPubkey === selectedConversationKey) || (!onSame && ongoingCallPubkey !== selectedConversationKey) ) { - CallManager.addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => { + addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => { const { audioInputsList, audioOutputsList, @@ -68,7 +69,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { } return () => { - CallManager.removeVideoEventsListener(uniqueId); + removeVideoEventsListener(uniqueId); }; }, [ongoingCallPubkey, selectedConversationKey, isFullScreen]); diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/calling/CallManager.ts similarity index 95% rename from ts/session/utils/CallManager.ts rename to ts/session/utils/calling/CallManager.ts index d4d068950..b84c0e776 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; -import { MessageUtils, ToastUtils, UserUtils } from '.'; -import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; -import { getConversationById } from '../../data/data'; -import { MessageModelType } from '../../models/messageType'; -import { SignalService } from '../../protobuf'; -import { openConversationWithMessages } from '../../state/ducks/conversations'; +import { MessageUtils, ToastUtils, UserUtils } from '../'; +import { getCallMediaPermissionsSettings } from '../../../components/session/settings/SessionSettings'; +import { getConversationById } from '../../../data/data'; +import { MessageModelType } from '../../../models/messageType'; +import { SignalService } from '../../../protobuf'; +import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { answerCall, callConnected, @@ -13,22 +13,23 @@ import { incomingCall, setFullScreenCall, startingCallWith, -} from '../../state/ducks/call'; -import { getConversationController } from '../conversations'; -import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; -import { ed25519Str } from '../onions/onionPath'; -import { getMessageQueue, MessageSender } from '../sending'; -import { PubKey } from '../types'; +} from '../../../state/ducks/call'; +import { getConversationController } from '../../conversations'; +import { CallMessage } from '../../messages/outgoing/controlMessage/CallMessage'; +import { ed25519Str } from '../../onions/onionPath'; +import { PubKey } from '../../types'; import { v4 as uuidv4 } from 'uuid'; -import { PnServer } from '../../pushnotification'; -import { setIsRinging } from './RingingManager'; +import { PnServer } from '../../../pushnotification'; +import { setIsRinging } from '../RingingManager'; +import { getBlackSilenceMediaStream } from './Silence'; +import { getMessageQueue } from '../..'; +import { MessageSender } from '../../sending'; -export type InputItem = { deviceId: string; label: string }; // tslint:disable: function-name -const maxWidth = 1920; -const maxHeight = 1080; +export type InputItem = { deviceId: string; label: string }; + /** * This uuid is set only once we accepted a call or started one. */ @@ -169,28 +170,6 @@ if (typeof navigator !== 'undefined') { }); } -const silence = () => { - const ctx = new AudioContext(); - const oscillator = ctx.createOscillator(); - const dst = oscillator.connect(ctx.createMediaStreamDestination()); - oscillator.start(); - return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false }); -}; - -const black = () => { - const canvas = Object.assign(document.createElement('canvas'), { - width: maxWidth, - height: maxHeight, - }); - canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight); - const stream = (canvas as any).captureStream(); - return Object.assign(stream.getVideoTracks()[0], { enabled: false }); -}; - -const getBlackSilenceMediaStream = () => { - return new MediaStream([black(), silence()]); -}; - async function updateConnectedDevices() { // Get the set of cameras connected const videoCameras = await getConnectedDevices('videoinput'); @@ -339,12 +318,13 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { if (!peerConnection) { throw new Error('cannot selectAudioInputByDeviceId without a peer connection'); } - const sender = peerConnection.getSenders().find(s => { + const audioSender = peerConnection.getSenders().find(s => { return s.track?.kind === audioTrack.kind; }); + window.log.info('replacing audio track'); - if (sender) { - await sender.replaceTrack(audioTrack); + if (audioSender) { + await audioSender.replaceTrack(audioTrack); // we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves) } else { throw new Error('Failed to get sender for selectAudioInputByDeviceId '); @@ -562,6 +542,11 @@ function handleConnectionStateChanged(pubkey: string) { if (firstAudioInput) { void selectAudioInputByDeviceId(firstAudioInput); } + + const firstAudioOutput = audioOutputsList?.[0].deviceId || undefined; + if (firstAudioOutput) { + void selectAudioOutputByDeviceId(firstAudioOutput); + } window.inboxStore?.dispatch(callConnected({ pubkey })); } } diff --git a/ts/session/utils/calling/Silence.ts b/ts/session/utils/calling/Silence.ts new file mode 100644 index 000000000..9d86488de --- /dev/null +++ b/ts/session/utils/calling/Silence.ts @@ -0,0 +1,24 @@ +const maxWidth = 1920; +const maxHeight = 1080; + +const silence = () => { + const ctx = new AudioContext(); + const oscillator = ctx.createOscillator(); + const dst = oscillator.connect(ctx.createMediaStreamDestination()); + oscillator.start(); + return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false }); +}; + +const black = () => { + const canvas = Object.assign(document.createElement('canvas'), { + width: maxWidth, + height: maxHeight, + }); + canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight); + const stream = (canvas as any).captureStream(); + return Object.assign(stream.getVideoTracks()[0], { enabled: false }); +}; + +export const getBlackSilenceMediaStream = () => { + return new MediaStream([black(), silence()]); +}; diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index 6bc07f0dc..19ab86ab3 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -8,7 +8,7 @@ import * as UserUtils from './User'; import * as SyncUtils from './syncUtils'; import * as AttachmentsV2Utils from './AttachmentsV2'; import * as AttachmentDownloads from './AttachmentsDownload'; -import * as CallManager from './CallManager'; +import * as CallManager from './calling/CallManager'; export * from './Attachments'; export * from './TypedEmitter'; From e716f73d6cf68ff8a64696f0af46bf0bf2a8f567 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 15:28:27 +1100 Subject: [PATCH 33/70] mute audio from bg when video is in fullscreen this is to avoid having two times the remote sound playing one in the bg and one in the fullscreen --- .../session/calling/InConversationCallContainer.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index f7d42944f..85d0384f3 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import _ from 'underscore'; import { UserUtils } from '../../../session/utils'; import { + getCallIsInFullScreen, getHasOngoingCallWith, getHasOngoingCallWithFocusedConvo, getHasOngoingCallWithFocusedConvoIsOffering, @@ -119,6 +120,8 @@ export const VideoLoadingSpinner = (props: { fullWidth: boolean }) => { export const InConversationCallContainer = () => { const ongoingCallProps = useSelector(getHasOngoingCallWith); + const isInFullScreen = useSelector(getCallIsInFullScreen); + const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo); const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name; @@ -158,12 +161,17 @@ export const InConversationCallContainer = () => { if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) { videoRefRemote.current.muted = true; } else { - // void videoRefRemote.current.setSinkId(currentSelectedAudioOutput); + void (videoRefRemote.current as any)?.setSinkId(currentSelectedAudioOutput); videoRefRemote.current.muted = false; } } } + if (isInFullScreen && videoRefRemote.current) { + // disable this video element so the one in fullscreen is the only one playing audio + videoRefRemote.current.muted = true; + } + if (!ongoingCallWithFocused) { return null; } From 9c9a43ee97f4982088c9fe0456c62ac6d040f761 Mon Sep 17 00:00:00 2001 From: warrickct Date: Mon, 22 Nov 2021 15:48:12 +1100 Subject: [PATCH 34/70] Adding improvements to message request handling. --- _locales/en/messages.json | 3 +- ts/components/LeftPane.tsx | 1 + .../session/LeftPaneMessageSection.tsx | 13 +- .../session/settings/SessionSettings.tsx | 20 +-- ts/receiver/configMessage.ts | 2 +- ts/state/ducks/userConfig.tsx | 14 +- ts/state/selectors/conversations.ts | 131 ++++-------------- 7 files changed, 46 insertions(+), 138 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index dba5ec8dc..1dc5dc183 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -447,5 +447,6 @@ "blockAll": "Block All", "messageRequests": "Message Requests", "requestsSubtitle": "Pending Requests", - "requestsPlaceholder": "No requests" + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox" } diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 271485155..30623f91f 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -34,6 +34,7 @@ const InnerLeftPaneMessageSection = () => { diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index b78208892..d47566c98 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -37,6 +37,7 @@ export interface Props { contacts: Array; conversations?: Array; + conversationRequests?: Array; searchResults?: SearchResultsProps; } @@ -281,13 +282,19 @@ export class LeftPaneMessageSection extends React.Component { this.handleToggleOverlay(undefined); }} onButtonClick={async () => { + window?.log?.info('Blocking all conversations'); // TODO: msgrequest iterate all convos and block // iterate all conversations and set all to approve then - const allConversations = getConversationController().getConversations(); + const { conversationRequests } = this.props; let syncRequired = false; - _.forEach(allConversations, convo => { - if (convo.isApproved() !== true) { + if (!conversationRequests) { + window?.log?.info('No conversation requests to block.'); + return; + } + + _.forEach(conversationRequests, convo => { + if (convo.isApproved !== true) { BlockedNumberController.block(convo.id); syncRequired = true; } diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 5c6228ee0..8e4df3739 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -17,11 +17,7 @@ import { import { shell } from 'electron'; import { mapDispatchToProps } from '../../../state/actions'; import { unblockConvoById } from '../../../interactions/conversationInteractions'; -import { - disableMessageRequests, - enableMessageRequests, - toggleAudioAutoplay, -} from '../../../state/ducks/userConfig'; +import { toggleAudioAutoplay, toggleMessageRequests } from '../../../state/ducks/userConfig'; import { sessionPassword, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { PasswordAction } from '../../dialog/SessionPasswordDialog'; import { SessionIconButton } from '../icon'; @@ -412,22 +408,16 @@ class SettingsViewInner extends React.Component { }, { id: 'message-request-setting', - title: 'Message Requests', // TODO: translation - description: 'Enable Message Request Inbox', + title: window.i18n('messageRequests'), + description: window.i18n('messageRequestsDescription'), hidden: false, type: SessionSettingType.Toggle, category: SessionSettingCategory.Appearance, setFn: () => { - window.inboxStore?.dispatch(toggleAudioAutoplay()); - - if (window.inboxStore?.getState().userConfig.messageRequests) { - window.inboxStore?.dispatch(disableMessageRequests()); - } else { - window.inboxStore?.dispatch(enableMessageRequests()); - } + window.inboxStore?.dispatch(toggleMessageRequests()); }, content: { - defaultValue: window.inboxStore?.getState().userConfig.audioAutoplay, + defaultValue: window.inboxStore?.getState().userConfig.messageRequests, }, comparisonValue: undefined, onClick: undefined, diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 37f0e2637..eb7ce798b 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -125,7 +125,7 @@ const handleContactReceived = async ( // updateProfile will do a commit for us contactConvo.set('active_at', _.toNumber(envelope.timestamp)); - if (window.lokiFeatureFlags.useMessageRequests === true) { + if (window.lokiFeatureFlags.useMessageRequests === true && window.inboxStore?.getState().userConfig.messageRequests) { contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); if (contactReceived.isBlocked === true) { diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index 8628dcd20..57af32fdc 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -26,20 +26,12 @@ const userConfigSlice = createSlice({ disableRecoveryPhrasePrompt: state => { state.showRecoveryPhrasePrompt = false; }, - disableMessageRequests: state => { - state.messageRequests = false; - }, - enableMessageRequests: state => { - state.messageRequests = false; + toggleMessageRequests: state => { + state.messageRequests = !state.messageRequests; }, }, }); const { actions, reducer } = userConfigSlice; -export const { - toggleAudioAutoplay, - disableRecoveryPhrasePrompt, - disableMessageRequests, - enableMessageRequests, -} = actions; +export const { toggleAudioAutoplay, disableRecoveryPhrasePrompt, toggleMessageRequests } = actions; export const userConfigReducer = reducer; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index bf9f20e52..7c76033b9 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -304,49 +304,44 @@ export const _getLeftPaneLists = ( }; } + // TODO: if message requests toggle on and msg requesnt enable const messageRequestsEnabled = window.inboxStore?.getState().userConfig.messageRequests === true && - window?.lokiFeatureFlags?.useMessageRequests === true; - - // if (!Boolean(conversation.isApproved) === true && window.lokiFeatureFlags.useMessageRequests) { - // continue; - // } + window.lokiFeatureFlags?.useMessageRequests === true; // Add Open Group to list as soon as the name has been set - // if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { - // continue; - // } - - // if (!conversation.isApproved && !conversation.isBlocked) { - // conversationRequests.push(conversation); - // } - - if (shouldShowInRequestList(conversation, messageRequestsEnabled)) { - conversationRequests.push(conversation); + if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { + continue; } // Remove all invalid conversations and conversatons of devices associated // with cancelled attempted links - // if (!conversation.isPublic && !conversation.activeAt) { - // continue; - // } - - // if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { - // directConversations.push(conversation); - // } + if (!conversation.isPublic && !conversation.activeAt) { + continue; + } - if (shouldShowInContacts(conversation)) { + if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { directConversations.push(conversation); } - if (shouldShowInConversationList(conversation, messageRequestsEnabled)) { - conversations.push(conversation); - unreadCount = calculateNewUnreadTotal(unreadCount, conversation); + if (messageRequestsEnabled) { + if (!conversation.isApproved && !conversation.isBlocked) { + // dont increase unread counter, don't push to convo list. + conversationRequests.push(conversation); + continue; + } + } + + if ( + unreadCount < 9 && + conversation.unreadCount && + conversation.unreadCount > 0 && + conversation.currentNotificationSetting !== 'disabled' + ) { + unreadCount += conversation.unreadCount; } - // if (conversation.isApproved) { - // conversations.push(conversation); - // } + conversations.push(conversation); } return { @@ -357,84 +352,6 @@ export const _getLeftPaneLists = ( }; }; -const calculateNewUnreadTotal = (unreadCount: number, conversation: ReduxConversationType) => { - if ( - unreadCount < 9 && - conversation.unreadCount && - conversation.unreadCount > 0 && - conversation.currentNotificationSetting !== 'disabled' - ) { - unreadCount += conversation.unreadCount; - } - return unreadCount; -}; - -const shouldShowInRequestList = ( - conversation: ReduxConversationType, - messageRequestsEnabled: boolean -) => { - if (conversation.isPublic || conversation.isBlocked || Boolean(conversation.activeAt) === false) { - return false; - } - - if (messageRequestsEnabled) { - if (Boolean(conversation.isApproved || !conversation.isBlocked)) { - return false; - } - } - return true; -}; - -const shouldShowInContacts = (conversation: ReduxConversationType) => { - // Add Open Group to list as soon as the name has been set - if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { - return false; - } - - // if (shouldShowInRequestList(conversation)) { - // conversationRequests.push(conversation); - // } - - // Remove all invalid conversations and conversatons of devices associated - // with cancelled attempted links - if (!conversation.isPublic && !conversation.activeAt) { - return false; - } - - if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { - // directConversations.push(conversation); - return true; - } else { - return false; - } -}; - -const shouldShowInConversationList = ( - conversation: ReduxConversationType, - messageRequestsEnabled: boolean -) => { - // // Add Open Group to list as soon as the name has been set - if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { - return false; - } - - // Remove all invalid conversations and conversatons of devices associated - // with cancelled attempted links - if (!conversation.isPublic && !conversation.activeAt) { - return false; - } - - // if (!conversation.activeAt) { - // return false; - // } - - if (messageRequestsEnabled && !conversation.isApproved) { - return false; - } - - return true; -}; - export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, From 23ca19b125bf42319050448f52b26386ec09b18d Mon Sep 17 00:00:00 2001 From: warrickct Date: Mon, 22 Nov 2021 16:18:19 +1100 Subject: [PATCH 35/70] Only updating approval when it is a true value as we consider a block a decline. --- ts/models/conversation.ts | 2 +- ts/receiver/configMessage.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index c05077662..e30ff986e 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1517,7 +1517,7 @@ export class ConversationModel extends Backbone.Model { } public isApproved() { - return Boolean(this.get('isApproved')); + return this.get('isApproved'); } public getTitle() { diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index eb7ce798b..297a8ea79 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -125,7 +125,11 @@ const handleContactReceived = async ( // updateProfile will do a commit for us contactConvo.set('active_at', _.toNumber(envelope.timestamp)); - if (window.lokiFeatureFlags.useMessageRequests === true && window.inboxStore?.getState().userConfig.messageRequests) { + if ( + window.lokiFeatureFlags.useMessageRequests === true && + window.inboxStore?.getState().userConfig.messageRequests && + contactReceived.isApproved === true + ) { contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); if (contactReceived.isBlocked === true) { From 2144a3980fad308ed781c6894bfa1ed3a32d9728 Mon Sep 17 00:00:00 2001 From: warrickct Date: Mon, 22 Nov 2021 16:48:30 +1100 Subject: [PATCH 36/70] Linting and formatting. --- ts/components/session/LeftPaneMessageSection.tsx | 16 +++++++--------- ts/components/session/SessionClosableOverlay.tsx | 10 ++-------- ts/models/conversation.ts | 12 +++++++----- ts/receiver/configMessage.ts | 2 +- ts/receiver/queuedJob.ts | 4 ++-- ts/session/utils/syncUtils.ts | 14 +++++--------- 6 files changed, 24 insertions(+), 34 deletions(-) diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index d47566c98..c5c356c43 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -181,11 +181,6 @@ export class LeftPaneMessageSection extends React.Component { ); } - private handleMessageRequestsClick() { - console.warn('handle msg req clicked'); - this.handleToggleOverlay(SessionClosableOverlayType.MessageRequests); - } - public updateSearch(searchTerm: string) { if (!searchTerm) { window.inboxStore?.dispatch(clearSearch()); @@ -224,6 +219,10 @@ export class LeftPaneMessageSection extends React.Component { ); } + private handleMessageRequestsClick() { + this.handleToggleOverlay(SessionClosableOverlayType.MessageRequests); + } + private renderClosableOverlay() { const { searchTerm, searchResults } = this.props; const { loading, overlay } = this.state; @@ -282,9 +281,8 @@ export class LeftPaneMessageSection extends React.Component { this.handleToggleOverlay(undefined); }} onButtonClick={async () => { + // block all convo requests. Force sync if there were changes. window?.log?.info('Blocking all conversations'); - // TODO: msgrequest iterate all convos and block - // iterate all conversations and set all to approve then const { conversationRequests } = this.props; let syncRequired = false; @@ -293,9 +291,9 @@ export class LeftPaneMessageSection extends React.Component { return; } - _.forEach(conversationRequests, convo => { + _.forEach(conversationRequests, async convo => { if (convo.isApproved !== true) { - BlockedNumberController.block(convo.id); + await BlockedNumberController.block(convo.id); syncRequired = true; } }); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index c5b532aa1..ad4edeb0e 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -199,7 +199,6 @@ export class SessionClosableOverlay extends React.Component { {isMessageRequestView ? ( <> -
@@ -301,7 +300,7 @@ const MessageRequestList = () => { return (
{validConversationRequests.map(conversation => { - return ; + return ; })}
); @@ -309,10 +308,5 @@ const MessageRequestList = () => { const MessageRequestListItem = (props: { conversation: ConversationListItemProps }) => { const { conversation } = props; - return ( - - ); + return ; }; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index e30ff986e..7fc08d049 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -50,7 +50,7 @@ import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsMana import { IMAGE_JPEG } from '../types/MIME'; import { UnsendMessage } from '../session/messages/outgoing/controlMessage/UnsendMessage'; import { getLatestTimestampOffset, networkDeleteMessages } from '../session/snode_api/SNodeAPI'; -import { syncConfigurationIfNeeded } from '../session/utils/syncUtils'; +import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; export enum ConversationTypeEnum { GROUP = 'group', @@ -738,8 +738,8 @@ export class ConversationModel extends Backbone.Model { const updateApprovalNeeded = !this.isApproved() && (this.isPrivate() || this.isMediumGroup() || this.isClosedGroup()); if (updateApprovalNeeded) { - this.setIsApproved(true); - await syncConfigurationIfNeeded(true); + await this.setIsApproved(true); + await forceSyncConfigurationNowIfNeeded(); } if (this.isOpenGroupV2()) { @@ -1397,13 +1397,15 @@ export class ConversationModel extends Backbone.Model { public async setIsApproved(value: boolean) { if (value !== this.get('isApproved')) { - console.warn(`Setting ${this.attributes.profileName} isApproved to:: ${value}`); + window?.log?.info(`Setting ${this.attributes.profileName} isApproved to:: ${value}`); this.set({ isApproved: value, }); // to exclude the conversation from left pane messages list and message requests - if (value === false) this.set({ active_at: undefined }); + if (value === false) { + this.set({ active_at: undefined }); + } await this.commit(); } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 297a8ea79..4a2fb4d67 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -130,7 +130,7 @@ const handleContactReceived = async ( window.inboxStore?.getState().userConfig.messageRequests && contactReceived.isApproved === true ) { - contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); + await contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); if (contactReceived.isBlocked === true) { await BlockedNumberController.block(contactConvo.id); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 9be31a3a8..ca57e91cd 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -314,8 +314,8 @@ async function handleRegularMessage( if (type === 'outgoing' && window.lokiFeatureFlags.useMessageRequests) { handleSyncedReceipts(message, conversation); - // assumes sync receipts are always from linked device outgoings? - conversation.setIsApproved(true); + // assumes sync receipts are always from linked device outgoings + await conversation.setIsApproved(true); } const conversationActiveAt = conversation.get('active_at'); diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 9c6017bd5..854be417c 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -38,23 +38,20 @@ const writeLastSyncTimestampToDb = async (timestamp: number) => createOrUpdateItem({ id: ITEM_ID_LAST_SYNC_TIMESTAMP, value: timestamp }); /** - * Syncs usre configuration with other devices linked to this user. - * @param force Bypass duration time limit for sending sync messages - * @returns + * Conditionally Syncs user configuration with other devices linked. */ -export const syncConfigurationIfNeeded = async (force: boolean = false) => { +export const syncConfigurationIfNeeded = async () => { const lastSyncedTimestamp = (await getLastSyncTimestampFromDb()) || 0; const now = Date.now(); // if the last sync was less than 2 days before, return early. - if (!force && Math.abs(now - lastSyncedTimestamp) < DURATION.DAYS * 7) { + if (Math.abs(now - lastSyncedTimestamp) < DURATION.DAYS * 7) { return; } - const allConvos = await (await getAllConversations()).models; + const allConvoCollection = await getAllConversations(); + const allConvos = allConvoCollection.models; - console.warn({ test: allConvos[0].attributes.isApproved }); - // const configMessage = await getCurrentConfigurationMessage(allConvos); const configMessage = await getCurrentConfigurationMessage(allConvos); try { // window?.log?.info('syncConfigurationIfNeeded with', configMessage); @@ -71,7 +68,6 @@ export const syncConfigurationIfNeeded = async (force: boolean = false) => { export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = false) => new Promise(async resolve => { - // const allConvos = getConversationController().getConversations(); const allConvos = (await getAllConversations()).models; // if we hang for more than 10sec, force resolve this promise. From f0161ec338f123521512187eb2299fee035ef09f Mon Sep 17 00:00:00 2001 From: warrickct Date: Mon, 22 Nov 2021 16:59:43 +1100 Subject: [PATCH 37/70] More formatting and linting --- ts/state/selectors/conversations.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 7c76033b9..c4fd02ba6 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -268,6 +268,7 @@ export const _getConversationComparator = (testingi18n?: LocalizerType) => { export const getConversationComparator = createSelector(getIntl, _getConversationComparator); // export only because we use it in some of our tests +// tslint:disable-next-line: cyclomatic-complexity export const _getLeftPaneLists = ( lookup: ConversationLookupType, comparator: (left: ReduxConversationType, right: ReduxConversationType) => number, @@ -304,10 +305,13 @@ export const _getLeftPaneLists = ( }; } + let messageRequestsEnabled = false; // TODO: if message requests toggle on and msg requesnt enable - const messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true && - window.lokiFeatureFlags?.useMessageRequests === true; + if (window?.inboxStore?.getState()) { + messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true && + window.lokiFeatureFlags?.useMessageRequests === true; + } // Add Open Group to list as soon as the name has been set if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { From 043c2fa99f266d39fb109c3d2f7714d796da3eea Mon Sep 17 00:00:00 2001 From: warrickct Date: Mon, 22 Nov 2021 17:25:28 +1100 Subject: [PATCH 38/70] fixing merge conflicts --- .../session/settings/section/CategoryPrivacy.tsx | 12 ++++++++++++ ts/window.d.ts | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx index a0565fc83..da4e7c02f 100644 --- a/ts/components/session/settings/section/CategoryPrivacy.tsx +++ b/ts/components/session/settings/section/CategoryPrivacy.tsx @@ -2,6 +2,7 @@ import React from 'react'; // tslint:disable-next-line: no-submodule-imports import useUpdate from 'react-use/lib/useUpdate'; import { sessionPassword, updateConfirmModal } from '../../../../state/ducks/modalDialog'; +import { toggleMessageRequests } from '../../../../state/ducks/userConfig'; import { PasswordAction } from '../../../dialog/SessionPasswordDialog'; import { SessionButtonColor } from '../../SessionButton'; import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem'; @@ -107,6 +108,17 @@ export const SettingsCategoryPrivacy = (props: { description={window.i18n('autoUpdateSettingDescription')} active={Boolean(window.getSettingValue(settingsAutoUpdate))} /> + { + // const old = Boolean(window.getSettingValue(settingsAutoUpdate)); + // window.setSettingValue(settingsAutoUpdate, !old); + window.inboxStore?.dispatch(toggleMessageRequests()); + forceUpdate(); + }} + title={window.i18n('messageRequests')} + description={window.i18n('messageRequestsDescription')} + active={Boolean(window.getSettingValue(settingsAutoUpdate))} + /> {!props.hasPassword && ( Date: Mon, 22 Nov 2021 17:36:23 +1100 Subject: [PATCH 39/70] linting and formatting changes --- ts/components/ConversationListItem.tsx | 1 + ts/receiver/queuedJob.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 8031916b2..cba97902f 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -257,6 +257,7 @@ const AvatarItem = (props: { ); }; +// tslint:disable: max-func-body-length const ConversationListItem = (props: Props) => { const { activeAt, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 17edc225b..cdf3cbd61 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -318,7 +318,7 @@ async function handleRegularMessage( } if (type === 'outgoing' && window.lokiFeatureFlags.useMessageRequests) { - handleSyncedReceipts(message, conversation); + await handleSyncedReceipts(message, conversation); // assumes sync receipts are always from linked device outgoings await conversation.setIsApproved(true); From 8fea533124a2a23a32ac1432dbfde8b683ccaf2a Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 09:50:17 +1100 Subject: [PATCH 40/70] darken a bit the green of sent message box in light theme --- ts/state/ducks/SessionTheme.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx index df22031c7..1f73c9bdc 100644 --- a/ts/state/ducks/SessionTheme.tsx +++ b/ts/state/ducks/SessionTheme.tsx @@ -149,7 +149,7 @@ const lightColorTextSubtle = `${black}99`; const lightColorTextAccent = '#00c769'; const lightColorSessionShadow = `0 0 4px 0 ${black}5E`; const lightColorComposeViewBg = '#efefef'; -const lightColorSentMessageBg = accentLightTheme; +const lightColorSentMessageBg = 'hsl(152, 100%, 40%)'; const lightColorSentMessageText = white; const lightColorClickableHovered = '#dfdfdf'; const lightColorSessionBorderColor = borderLightThemeColor; From af75b6f0e293cf4cf1c7aef7e801ff93f442ae96 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 10:32:11 +1100 Subject: [PATCH 41/70] disable deduplication based serverId+sender only use the serverTimestamp+sender for searching because serverId+sender might have false positive --- app/sql.js | 16 ---------------- ts/data/data.ts | 19 ------------------- ts/receiver/dataMessage.ts | 18 +++--------------- 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/app/sql.js b/app/sql.js index 217f7ba56..a1d421450 100644 --- a/app/sql.js +++ b/app/sql.js @@ -60,7 +60,6 @@ module.exports = { getUnreadByConversation, getUnreadCountByConversation, getMessageBySender, - getMessageBySenderAndServerId, getMessageBySenderAndServerTimestamp, getMessageBySenderAndTimestamp, getMessageIdsFromServerIds, @@ -2058,21 +2057,6 @@ function getMessageBySender({ source, sourceDevice, sentAt }) { return map(rows, row => jsonToObject(row.json)); } -function getMessageBySenderAndServerId({ source, serverId }) { - const rows = globalInstance - .prepare( - `SELECT json FROM ${MESSAGES_TABLE} WHERE - source = $source AND - serverId = $serverId;` - ) - .all({ - source, - serverId, - }); - - return map(rows, row => jsonToObject(row.json)); -} - function getMessageBySenderAndTimestamp({ source, timestamp }) { const rows = globalInstance .prepare( diff --git a/ts/data/data.ts b/ts/data/data.ts index c8c912086..8665a9da0 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -110,7 +110,6 @@ const channelsToMake = { removeAllMessagesInConversation, getMessageBySender, - getMessageBySenderAndServerId, getMessageBySenderAndServerTimestamp, getMessageBySenderAndTimestamp, getMessageIdsFromServerIds, @@ -690,24 +689,6 @@ export async function getMessageBySender({ return new MessageModel(messages[0]); } -export async function getMessageBySenderAndServerId({ - source, - serverId, -}: { - source: string; - serverId: number; -}): Promise { - const messages = await channels.getMessageBySenderAndServerId({ - source, - serverId, - }); - if (!messages || !messages.length) { - return null; - } - - return new MessageModel(messages[0]); -} - export async function getMessageBySenderAndServerTimestamp({ source, serverTimestamp, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index b41265c09..f0619d434 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -12,11 +12,7 @@ import { getConversationController } from '../session/conversations'; import { handleClosedGroupControlMessage } from './closedGroups'; import { MessageModel } from '../models/message'; import { MessageModelType } from '../models/messageType'; -import { - getMessageBySender, - getMessageBySenderAndServerId, - getMessageBySenderAndServerTimestamp, -} from '../../ts/data/data'; +import { getMessageBySender, getMessageBySenderAndServerTimestamp } from '../../ts/data/data'; import { ConversationModel, ConversationTypeEnum } from '../models/conversation'; import { allowOnlyOneAtATime } from '../session/utils/Promise'; import { toHex } from '../session/utils/String'; @@ -368,22 +364,14 @@ export async function isMessageDuplicate({ try { let result; if (serverId || serverTimestamp) { - // first try to find a duplicate serverId from this sender - if (serverId) { - result = await getMessageBySenderAndServerId({ - source, - serverId, - }); - } - // if no result, try to find a duplicate with the same serverTimestamp from this sender + // first try to find a duplicate with the same serverTimestamp from this sender if (!result && serverTimestamp) { result = await getMessageBySenderAndServerTimestamp({ source, serverTimestamp, }); } - // if we have a result, it means a specific user sent two messages either with the same - // serverId or the same serverTimestamp. + // if we have a result, it means a specific user sent two messages either with the same serverTimestamp. // no need to do anything else, those messages must be the same // Note: this test is not based on which conversation the user sent the message // but we consider that a user sending two messages with the same serverTimestamp is unlikely From 3602b51986165756a4c4f72cc94d587d978f64bb Mon Sep 17 00:00:00 2001 From: warrickct Date: Tue, 23 Nov 2021 11:00:11 +1100 Subject: [PATCH 42/70] Fixing up block all logic. --- ts/components/session/LeftPaneMessageSection.tsx | 9 +++++---- ts/receiver/configMessage.ts | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index c5c356c43..89a17b140 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -291,12 +291,13 @@ export class LeftPaneMessageSection extends React.Component { return; } - _.forEach(conversationRequests, async convo => { - if (convo.isApproved !== true) { + await Promise.all( + conversationRequests.map(async convo => { await BlockedNumberController.block(convo.id); syncRequired = true; - } - }); + }) + ); + if (syncRequired) { await forceSyncConfigurationNowIfNeeded(); } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 4a2fb4d67..86a0de634 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -127,10 +127,11 @@ const handleContactReceived = async ( if ( window.lokiFeatureFlags.useMessageRequests === true && - window.inboxStore?.getState().userConfig.messageRequests && - contactReceived.isApproved === true + window.inboxStore?.getState().userConfig.messageRequests ) { - await contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); + if (contactReceived.isApproved === true) { + await contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); + } if (contactReceived.isBlocked === true) { await BlockedNumberController.block(contactConvo.id); From 5ba7f20162270a7644414259faec744fdef18108 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 15:18:46 +1100 Subject: [PATCH 43/70] speed up fetching closed group's members avatar --- js/background.js | 21 ---- ts/components/Avatar.tsx | 23 ++--- .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 96 +++++++++---------- ts/components/ConversationListItem.tsx | 10 +- .../conversation/ConversationHeader.tsx | 10 +- .../conversation/SessionRightPanel.tsx | 10 +- .../usingClosedConversationDetails.tsx | 69 ------------- ts/hooks/useMembersAvatar.tsx | 55 ----------- ts/hooks/useMembersAvatars.tsx | 55 +++++++++++ ts/state/selectors/conversations.ts | 1 + 10 files changed, 116 insertions(+), 234 deletions(-) delete mode 100644 ts/components/session/usingClosedConversationDetails.tsx delete mode 100644 ts/hooks/useMembersAvatar.tsx create mode 100644 ts/hooks/useMembersAvatars.tsx diff --git a/js/background.js b/js/background.js index 092edf848..bbf003ee3 100644 --- a/js/background.js +++ b/js/background.js @@ -192,27 +192,6 @@ } }); - Whisper.events.on('deleteLocalPublicMessages', async ({ messageServerIds, conversationId }) => { - if (!Array.isArray(messageServerIds)) { - return; - } - const messageIds = await window.Signal.Data.getMessageIdsFromServerIds( - messageServerIds, - conversationId - ); - if (messageIds.length === 0) { - return; - } - - const conversation = window.getConversationController().get(conversationId); - messageIds.forEach(id => { - if (conversation) { - conversation.removeMessage(id); - } - window.Signal.Data.removeMessage(id); - }); - }); - function manageExpiringData() { window.Signal.Data.cleanSeenMessages(); window.Signal.Data.cleanLastHashes(); diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 0c347cce8..8597d6850 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -2,9 +2,9 @@ import React, { useCallback, useState } from 'react'; import classNames from 'classnames'; import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder'; -import { ConversationAvatar } from './session/usingClosedConversationDetails'; import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; import _ from 'underscore'; +import { useMembersAvatars } from '../hooks/useMembersAvatars'; export enum AvatarSize { XS = 28, @@ -21,7 +21,6 @@ type Props = { pubkey?: string; size: AvatarSize; base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data - memberAvatars?: Array; // this is added by usingClosedConversationDetails onAvatarClick?: () => void; dataTestId?: string; }; @@ -42,21 +41,17 @@ const Identicon = (props: Props) => { }; const NoImage = (props: { - memberAvatars?: Array; name?: string; pubkey?: string; size: AvatarSize; + isClosedGroup: boolean; onAvatarClick?: () => void; }) => { - const { name, memberAvatars, size, pubkey } = props; + const { name, size, pubkey, isClosedGroup } = props; // if no image but we have conversations set for the group, renders group members avatars - if (memberAvatars) { + if (pubkey && isClosedGroup) { return ( - + ); } @@ -93,8 +88,10 @@ const AvatarImage = (props: { }; const AvatarInner = (props: Props) => { - const { avatarPath, base64Data, size, memberAvatars, name, dataTestId } = props; + const { avatarPath, base64Data, size, pubkey, name, dataTestId } = props; const [imageBroken, setImageBroken] = useState(false); + + const closedGroupMembers = useMembersAvatars(pubkey); // contentType is not important const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', ''); const handleImageError = () => { @@ -106,7 +103,7 @@ const AvatarInner = (props: Props) => { setImageBroken(true); }; - const isClosedGroupAvatar = Boolean(memberAvatars?.length); + const isClosedGroupAvatar = Boolean(closedGroupMembers?.length); const hasImage = (base64Data || urlToLoad) && !imageBroken && !isClosedGroupAvatar; const isClickable = !!props.onAvatarClick; @@ -134,7 +131,7 @@ const AvatarInner = (props: Props) => { handleImageError={handleImageError} /> ) : ( - + )}
); diff --git a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx index 4dc344564..be3ef30d1 100644 --- a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx +++ b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx @@ -1,59 +1,57 @@ import React from 'react'; +import { useMembersAvatars } from '../../hooks/useMembersAvatars'; import { Avatar, AvatarSize } from '../Avatar'; -import { ConversationAvatar } from '../session/usingClosedConversationDetails'; -interface Props { +type Props = { size: number; - memberAvatars: Array; // this is added by usingClosedConversationDetails + closedGroupId: string; onAvatarClick?: () => void; -} +}; -export class ClosedGroupAvatar extends React.PureComponent { - public getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize { - // Always use the size directly under the one requested - switch (size) { - case AvatarSize.S: - return AvatarSize.XS; - case AvatarSize.M: - return AvatarSize.S; - case AvatarSize.L: - return AvatarSize.M; - case AvatarSize.XL: - return AvatarSize.L; - case AvatarSize.HUGE: - return AvatarSize.XL; - default: - throw new Error(`Invalid size request for closed group avatar: ${size}`); - } +function getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize { + // Always use the size directly under the one requested + switch (size) { + case AvatarSize.XS: + return AvatarSize.XS; + case AvatarSize.S: + return AvatarSize.XS; + case AvatarSize.M: + return AvatarSize.S; + case AvatarSize.L: + return AvatarSize.M; + case AvatarSize.XL: + return AvatarSize.L; + case AvatarSize.HUGE: + return AvatarSize.XL; + default: + throw new Error(`Invalid size request for closed group avatar: ${size}`); } +} - public render() { - const { memberAvatars, size, onAvatarClick } = this.props; - const avatarsDiameter = this.getClosedGroupAvatarsSize(size); +export const ClosedGroupAvatar = (props: Props) => { + const { closedGroupId, size, onAvatarClick } = props; - const conv1 = memberAvatars.length > 0 ? memberAvatars[0] : undefined; - const conv2 = memberAvatars.length > 1 ? memberAvatars[1] : undefined; - const name1 = conv1?.name || conv1?.id || undefined; - const name2 = conv2?.name || conv2?.id || undefined; + const memberAvatars = useMembersAvatars(closedGroupId); + const avatarsDiameter = getClosedGroupAvatarsSize(size); + const firstMember = memberAvatars?.[0]; + const secondMember = memberAvatars?.[1]; - // use the 2 first members as group avatars - return ( -
- - -
- ); - } -} + return ( +
+ + +
+ ); +}; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 58c7401ae..f8b8a37d1 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -9,7 +9,6 @@ import { Timestamp } from './conversation/Timestamp'; import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; -import { ConversationAvatar } from './session/usingClosedConversationDetails'; import { MemoConversationListItemContextMenu } from './session/menu/ConversationListItemContextMenu'; import { createPortal } from 'react-dom'; import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus'; @@ -21,7 +20,6 @@ import { ReduxConversationType, } from '../state/ducks/conversations'; import _ from 'underscore'; -import { useMembersAvatars } from '../hooks/useMembersAvatar'; import { SessionIcon } from './session/icon'; import { useDispatch, useSelector } from 'react-redux'; import { SectionType } from '../state/ducks/section'; @@ -217,13 +215,11 @@ const MessageItem = (props: { const AvatarItem = (props: { avatarPath: string | null; conversationId: string; - memberAvatars?: Array; name?: string; profileName?: string; isPrivate: boolean; }) => { - const { avatarPath, name, isPrivate, conversationId, profileName, memberAvatars } = props; - + const { avatarPath, name, isPrivate, conversationId, profileName } = props; const userName = name || profileName || conversationId; const dispatch = useDispatch(); @@ -233,7 +229,6 @@ const AvatarItem = (props: { avatarPath={avatarPath} name={userName} size={AvatarSize.S} - memberAvatars={memberAvatars} pubkey={conversationId} onAvatarClick={() => { if (isPrivate) { @@ -278,8 +273,6 @@ const ConversationListItem = (props: Props) => { const triggerId = `conversation-item-${conversationId}-ctxmenu`; const key = `conversation-item-${conversationId}`; - const membersAvatar = useMembersAvatars(props); - const openConvo = useCallback( async (e: React.MouseEvent) => { // mousedown is invoked sooner than onClick, but for both right and left click @@ -319,7 +312,6 @@ const ConversationListItem = (props: Props) => { { const AvatarHeader = (props: { avatarPath: string | null; - memberAvatars?: Array; name?: string; pubkey: string; profileName?: string; showBackButton: boolean; onAvatarClick?: (pubkey: string) => void; }) => { - const { avatarPath, memberAvatars, name, pubkey, profileName } = props; + const { avatarPath, name, pubkey, profileName } = props; const userName = name || profileName || pubkey; return ( @@ -188,7 +184,6 @@ const AvatarHeader = (props: { props.onAvatarClick(pubkey); } }} - memberAvatars={memberAvatars} pubkey={pubkey} /> @@ -344,8 +339,6 @@ export const ConversationHeaderWithDetails = () => { const headerProps = useSelector(getConversationHeaderProps); const isSelectionMode = useSelector(isMessageSelectionMode); - const selectedConversation = useSelector(getSelectedConversation); - const memberDetails = useMembersAvatars(selectedConversation); const isMessageDetailOpened = useSelector(isMessageDetailView); const dispatch = useDispatch(); @@ -401,7 +394,6 @@ export const ConversationHeaderWithDetails = () => { pubkey={conversationKey} showBackButton={isMessageDetailOpened} avatarPath={avatarPath} - memberAvatars={memberDetails} name={name} profileName={profileName} /> diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index a0897b3ee..a2eb5d61c 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -31,7 +31,6 @@ import { getSelectedConversation, isRightPanelShowing, } from '../../../state/selectors/conversations'; -import { useMembersAvatars } from '../../../hooks/useMembersAvatar'; import { closeRightPanel } from '../../../state/ducks/conversations'; async function getMediaGalleryProps( @@ -110,7 +109,6 @@ async function getMediaGalleryProps( const HeaderItem = () => { const selectedConversation = useSelector(getSelectedConversation); const dispatch = useDispatch(); - const memberDetails = useMembersAvatars(selectedConversation); if (!selectedConversation) { return null; @@ -139,13 +137,7 @@ const HeaderItem = () => { dispatch(closeRightPanel()); }} /> - +
{showInviteContacts && ( ; // this is added by usingClosedConversationDetails -}; - -export function usingClosedConversationDetails(WrappedComponent: any) { - return class extends React.Component { - constructor(props: any) { - super(props); - this.state = { - memberAvatars: undefined, - }; - } - - public componentDidMount() { - this.fetchClosedConversationDetails(); - } - - public componentWillReceiveProps() { - this.fetchClosedConversationDetails(); - } - - public render() { - return ; - } - - private fetchClosedConversationDetails() { - const { isPublic, type, conversationType, isGroup, id } = this.props; - - if (!isPublic && (conversationType === 'group' || type === 'group' || isGroup)) { - const groupId = id; - const ourPrimary = UserUtils.getOurPubKeyFromCache(); - let members = GroupUtils.getGroupMembers(PubKey.cast(groupId)); - - const ourself = members.find(m => m.key !== ourPrimary.key); - // add ourself back at the back, so it's shown only if only 1 member and we are still a member - members = members.filter(m => m.key !== ourPrimary.key); - members.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); - if (ourself) { - members.push(ourPrimary); - } - // no need to forward more than 2 conversations for rendering the group avatar - members = members.slice(0, 2); - const memberConvos = _.compact(members.map(m => getConversationController().get(m.key))); - const memberAvatars = memberConvos.map(m => { - return { - avatarPath: m.getAvatarPath() || undefined, - id: m.id, - name: m.get('name') || m.get('profileName') || m.id, - }; - }); - this.setState({ memberAvatars }); - } else { - this.setState({ memberAvatars: undefined }); - } - } - }; -} diff --git a/ts/hooks/useMembersAvatar.tsx b/ts/hooks/useMembersAvatar.tsx deleted file mode 100644 index 76386fe83..000000000 --- a/ts/hooks/useMembersAvatar.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import _ from 'lodash'; -import { useEffect, useState } from 'react'; -import { getConversationController } from '../session/conversations'; -import { UserUtils } from '../session/utils'; -import { ReduxConversationType } from '../state/ducks/conversations'; - -export function useMembersAvatars(conversation: ReduxConversationType | undefined) { - const [membersAvatars, setMembersAvatars] = useState< - | Array<{ - avatarPath: string | undefined; - id: string; - name: string; - }> - | undefined - >(undefined); - - useEffect( - () => { - if (!conversation) { - setMembersAvatars(undefined); - return; - } - const { isPublic, isGroup, members: convoMembers } = conversation; - if (!isPublic && isGroup) { - const ourPrimary = UserUtils.getOurPubKeyStrFromCache(); - - const ourself = convoMembers?.find(m => m !== ourPrimary) || undefined; - // add ourself back at the back, so it's shown only if only 1 member and we are still a member - let membersFiltered = convoMembers?.filter(m => m !== ourPrimary) || []; - membersFiltered.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); - if (ourself) { - membersFiltered.push(ourPrimary); - } - // no need to forward more than 2 conversations for rendering the group avatar - membersFiltered = membersFiltered.slice(0, 2); - const memberConvos = _.compact( - membersFiltered.map(m => getConversationController().get(m)) - ); - const memberAvatars = memberConvos.map(m => { - return { - avatarPath: m.getAvatarPath() || undefined, - id: m.id as string, - name: (m.get('name') || m.get('profileName') || m.id) as string, - }; - }); - setMembersAvatars(memberAvatars); - } else { - setMembersAvatars(undefined); - } - }, - conversation ? [conversation.members, conversation.id] : [] - ); - - return membersAvatars; -} diff --git a/ts/hooks/useMembersAvatars.tsx b/ts/hooks/useMembersAvatars.tsx new file mode 100644 index 000000000..39e972a21 --- /dev/null +++ b/ts/hooks/useMembersAvatars.tsx @@ -0,0 +1,55 @@ +import { UserUtils } from '../session/utils'; +import * as _ from 'lodash'; +import { useSelector } from 'react-redux'; +import { StateType } from '../state/reducer'; + +export type ConversationAvatar = { + avatarPath?: string; + id: string; // member's pubkey + name: string; +}; + +export function useMembersAvatars(closedGroupPubkey: string | undefined) { + const ourPrimary = UserUtils.getOurPubKeyStrFromCache(); + + return useSelector((state: StateType): Array | undefined => { + if (!closedGroupPubkey) { + return undefined; + } + const groupConvo = state.conversations.conversationLookup[closedGroupPubkey]; + + if (groupConvo.isPrivate || groupConvo.isPublic || !groupConvo.isGroup) { + return undefined; + } + // this must be a closed group + const originalMembers = groupConvo.members; + if (!originalMembers || originalMembers.length === 0) { + return undefined; + } + const allMembersSorted = originalMembers.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + + // no need to forward more than 2 conversations for rendering the group avatar + const usAtTheEndMaxTwo = _.sortBy(allMembersSorted, a => (a === ourPrimary ? 1 : 0)).slice( + 0, + 2 + ); + const memberConvos = _.compact( + usAtTheEndMaxTwo + .map(m => state.conversations.conversationLookup[m]) + .map(m => { + if (!m) { + return undefined; + } + const userName = m.name || m.profileName || m.id; + + return { + avatarPath: m.avatarPath || undefined, + id: m.id, + name: userName, + }; + }) + ); + + return memberConvos && memberConvos.length ? memberConvos : undefined; + }); +} diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index de9fc35a4..16016868a 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -325,6 +325,7 @@ export const _getConversationComparator = (testingi18n?: LocalizerType) => { return collator.compare(leftTitle, rightTitle); }; }; + export const getConversationComparator = createSelector(getIntl, _getConversationComparator); // export only because we use it in some of our tests From 2d664a2df759cb3b1b0c3b84cbf87dbdce3ac893 Mon Sep 17 00:00:00 2001 From: warrickct Date: Tue, 23 Nov 2021 16:03:24 +1100 Subject: [PATCH 44/70] Applying PR changes. --- ts/components/ConversationListItem.tsx | 6 +- ts/components/LeftPane.tsx | 6 +- .../session/LeftPaneMessageSection.tsx | 2 - .../session/MessageRequestsBanner.tsx | 8 +- .../session/SessionClosableOverlay.tsx | 7 +- ts/receiver/queuedJob.ts | 8 +- ts/state/selectors/conversations.ts | 79 ++++++++++++++++--- ts/window.d.ts | 5 -- 8 files changed, 88 insertions(+), 33 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index cba97902f..4cd52f7e8 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -377,15 +377,15 @@ const ConversationListItem = (props: Props) => { Block - Accept - + text={window.i18n('accept')} + > ) : null}
diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 30623f91f..81beaef98 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -8,7 +8,7 @@ import { LeftPaneSettingSection } from './session/LeftPaneSettingSection'; import { SessionTheme } from '../state/ducks/SessionTheme'; import { getFocusedSection } from '../state/selectors/section'; import { useSelector } from 'react-redux'; -import { getLeftPaneLists } from '../state/selectors/conversations'; +import { getConversationRequests, getLeftPaneLists } from '../state/selectors/conversations'; import { getQuery, getSearchResults, isSearching } from '../state/selectors/search'; import { SectionType } from '../state/ducks/section'; @@ -29,12 +29,14 @@ const InnerLeftPaneMessageSection = () => { const searchResults = showSearch ? useSelector(getSearchResults) : undefined; const lists = showSearch ? undefined : useSelector(getLeftPaneLists); + const conversationRequests = useSelector(getConversationRequests); + // tslint:disable: use-simple-attributes return ( diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 89a17b140..c12ea5843 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -65,8 +65,6 @@ export class LeftPaneMessageSection extends React.Component { public constructor(props: Props) { super(props); - console.warn('convos updated'); - this.state = { loading: false, overlay: false, diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index f281bface..7e2f5408e 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { getLeftPaneLists } from '../../state/selectors/conversations'; +import { getConversationRequests } from '../../state/selectors/conversations'; import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; const StyledMessageRequestBanner = styled.div` @@ -83,9 +83,9 @@ export const CirclularIcon = (props: { iconType: SessionIconType; iconSize: Sess export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { const { handleOnClick } = props; - const convos = useSelector(getLeftPaneLists).conversationRequests; + const conversationRequests = useSelector(getConversationRequests); - if (!convos.length) { + if (!conversationRequests.length) { return null; } @@ -94,7 +94,7 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { Message Requests -
{convos.length || 0}
+
{conversationRequests.length || 0}
); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index ad4edeb0e..a533d09a2 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -10,7 +10,7 @@ import { ConversationTypeEnum } from '../../models/conversation'; import { SessionJoinableRooms } from './SessionJoinableDefaultRooms'; import { SpacerLG, SpacerMD } from '../basic/Text'; import { useSelector } from 'react-redux'; -import { getLeftPaneLists } from '../../state/selectors/conversations'; +import { getConversationRequests } from '../../state/selectors/conversations'; import { ConversationListItemProps, MemoConversationListItemWithDetails, @@ -295,11 +295,10 @@ export class SessionClosableOverlay extends React.Component { * @returns List of message request items */ const MessageRequestList = () => { - const lists = useSelector(getLeftPaneLists); - const validConversationRequests = lists?.conversationRequests; + const conversationRequests = useSelector(getConversationRequests); return (
- {validConversationRequests.map(conversation => { + {conversationRequests.map(conversation => { return ; })}
diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index cdf3cbd61..274770287 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -317,11 +317,13 @@ async function handleRegularMessage( updateReadStatus(message, conversation); } - if (type === 'outgoing' && window.lokiFeatureFlags.useMessageRequests) { + if (type === 'outgoing') { await handleSyncedReceipts(message, conversation); - // assumes sync receipts are always from linked device outgoings - await conversation.setIsApproved(true); + if (window.lokiFeatureFlags.useMessageRequests) { + // assumes sync receipts are always from linked device outgoings + await conversation.setIsApproved(true); + } } const conversationActiveAt = conversation.get('active_at'); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index bf1f80567..ed0022ab4 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -429,7 +429,6 @@ export const _getLeftPaneLists = ( ): { conversations: Array; contacts: Array; - conversationRequests: Array; unreadCount: number; } => { const values = Object.values(lookup); @@ -437,7 +436,6 @@ export const _getLeftPaneLists = ( const conversations: Array = []; const directConversations: Array = []; - const conversationRequests: Array = []; let unreadCount = 0; for (let conversation of sorted) { @@ -459,7 +457,7 @@ export const _getLeftPaneLists = ( } let messageRequestsEnabled = false; - // TODO: if message requests toggle on and msg requesnt enable + if (window?.inboxStore?.getState()) { messageRequestsEnabled = window.inboxStore?.getState().userConfig.messageRequests === true && @@ -481,12 +479,9 @@ export const _getLeftPaneLists = ( directConversations.push(conversation); } - if (messageRequestsEnabled) { - if (!conversation.isApproved && !conversation.isBlocked) { - // dont increase unread counter, don't push to convo list. - conversationRequests.push(conversation); - continue; - } + if (messageRequestsEnabled && !conversation.isApproved && !conversation.isBlocked) { + // dont increase unread counter, don't push to convo list. + continue; } if ( @@ -504,11 +499,75 @@ export const _getLeftPaneLists = ( return { conversations, contacts: directConversations, - conversationRequests, unreadCount, }; }; +export const _getConversationRequests = ( + lookup: ConversationLookupType, + comparator: (left: ReduxConversationType, right: ReduxConversationType) => number, + selectedConversation?: string +): Array => { + const values = Object.values(lookup); + const sorted = values.sort(comparator); + + const conversationRequests: Array = []; + + for (let conversation of sorted) { + if (selectedConversation === conversation.id) { + conversation = { + ...conversation, + isSelected: true, + }; + } + + const isBlocked = + BlockedNumberController.isBlocked(conversation.id) || + BlockedNumberController.isGroupBlocked(conversation.id); + + if (isBlocked) { + conversation = { + ...conversation, + isBlocked: true, + }; + } + + let messageRequestsEnabled = false; + + if (window?.inboxStore?.getState()) { + messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true && + window.lokiFeatureFlags?.useMessageRequests === true; + } + + // Add Open Group to list as soon as the name has been set + if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { + continue; + } + + // Remove all invalid conversations and conversatons of devices associated + // with cancelled attempted links + if (!conversation.isPublic && !conversation.activeAt) { + continue; + } + + if (messageRequestsEnabled && !conversation.isApproved && !conversation.isBlocked) { + // dont increase unread counter, don't push to convo list. + conversationRequests.push(conversation); + continue; + } + } + + return conversationRequests; +}; + +export const getConversationRequests = createSelector( + getConversationLookup, + getConversationComparator, + getSelectedConversationKey, + _getConversationRequests +); + export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, diff --git a/ts/window.d.ts b/ts/window.d.ts index 8ef04b728..389e29876 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -43,11 +43,6 @@ declare global { log: any; lokiFeatureFlags: { useOnionRequests: boolean; - useFileOnionRequests: boolean; - useFileOnionRequestsV2: boolean; - padOutgoingAttachments: boolean; - enablePinConversations: boolean; - useUnsendRequests: boolean; useMessageRequests: boolean; useCallMessage: boolean; }; From 894349e710d0e93ad10b988011a62c2cfc3017e5 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 16:18:27 +1100 Subject: [PATCH 45/70] cleanup props passing of avatar and name with a custom hook --- ts/components/Avatar.tsx | 37 +++++---- .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 22 +---- ts/components/ContactListItem.tsx | 82 ++++++++----------- ts/components/ConversationListItem.tsx | 65 ++++----------- .../conversation/ConversationHeader.tsx | 15 +--- ts/components/conversation/MessageDetail.tsx | 9 +- .../conversation/message/MessageAvatar.tsx | 8 +- ts/components/dialog/EditProfileDialog.tsx | 7 +- .../dialog/UpdateGroupNameDialog.tsx | 2 +- ts/components/dialog/UserDetailsDialog.tsx | 3 - ts/components/session/ActionsPanel.tsx | 19 +---- .../session/SessionMemberListItem.tsx | 40 +++------ .../calling/DraggableCallContainer.tsx | 10 +-- .../calling/InConversationCallContainer.tsx | 28 +------ .../session/calling/IncomingCallDialog.tsx | 10 +-- .../conversation/SessionRightPanel.tsx | 14 +--- .../menu/ConversationListItemContextMenu.tsx | 12 ++- ts/hooks/useMembersAvatars.tsx | 19 +---- ts/hooks/useParamSelector.ts | 17 +++- 19 files changed, 134 insertions(+), 285 deletions(-) diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 8597d6850..6b3537155 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -5,6 +5,7 @@ import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder'; import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; import _ from 'underscore'; import { useMembersAvatars } from '../hooks/useMembersAvatars'; +import { useAvatarPath, useConversationUsername } from '../hooks/useParamSelector'; export enum AvatarSize { XS = 28, @@ -16,8 +17,8 @@ export enum AvatarSize { } type Props = { - avatarPath?: string | null; - name?: string; // display name, profileName or pubkey, whatever is set first + forcedAvatarPath?: string | null; + forcedName?: string; pubkey?: string; size: AvatarSize; base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data @@ -26,8 +27,8 @@ type Props = { }; const Identicon = (props: Props) => { - const { size, name, pubkey } = props; - const userName = name || '0'; + const { size, forcedName, pubkey } = props; + const userName = forcedName || '0'; return ( { ); }; -const NoImage = (props: { - name?: string; - pubkey?: string; - size: AvatarSize; - isClosedGroup: boolean; - onAvatarClick?: () => void; -}) => { - const { name, size, pubkey, isClosedGroup } = props; +const NoImage = ( + props: Pick & { + isClosedGroup: boolean; + } +) => { + const { forcedName, size, pubkey, isClosedGroup } = props; // if no image but we have conversations set for the group, renders group members avatars if (pubkey && isClosedGroup) { return ( @@ -55,7 +54,7 @@ const NoImage = (props: { ); } - return ; + return ; }; const AvatarImage = (props: { @@ -88,17 +87,21 @@ const AvatarImage = (props: { }; const AvatarInner = (props: Props) => { - const { avatarPath, base64Data, size, pubkey, name, dataTestId } = props; + const { base64Data, size, pubkey, forcedAvatarPath, forcedName, dataTestId } = props; const [imageBroken, setImageBroken] = useState(false); const closedGroupMembers = useMembersAvatars(pubkey); + + const avatarPath = useAvatarPath(pubkey); + const name = useConversationUsername(pubkey); + // contentType is not important - const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', ''); + const { urlToLoad } = useEncryptedFileFetch(forcedAvatarPath || avatarPath || '', ''); const handleImageError = () => { window.log.warn( 'Avatar: Image failed to load; failing over to placeholder', urlToLoad, - avatarPath + forcedAvatarPath || avatarPath ); setImageBroken(true); }; @@ -127,7 +130,7 @@ const AvatarInner = (props: Props) => { avatarPath={urlToLoad} base64Data={base64Data} imageBroken={imageBroken} - name={name} + name={forcedName || name} handleImageError={handleImageError} /> ) : ( diff --git a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx index be3ef30d1..ac6fa155c 100644 --- a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx +++ b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx @@ -11,8 +11,6 @@ type Props = { function getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize { // Always use the size directly under the one requested switch (size) { - case AvatarSize.XS: - return AvatarSize.XS; case AvatarSize.S: return AvatarSize.XS; case AvatarSize.M: @@ -33,25 +31,13 @@ export const ClosedGroupAvatar = (props: Props) => { const memberAvatars = useMembersAvatars(closedGroupId); const avatarsDiameter = getClosedGroupAvatarsSize(size); - const firstMember = memberAvatars?.[0]; - const secondMember = memberAvatars?.[1]; + const firstMemberId = memberAvatars?.[0]; + const secondMemberID = memberAvatars?.[1]; return (
- - + +
); }; diff --git a/ts/components/ContactListItem.tsx b/ts/components/ContactListItem.tsx index c7deceba9..f16122dd5 100644 --- a/ts/components/ContactListItem.tsx +++ b/ts/components/ContactListItem.tsx @@ -3,55 +3,43 @@ import classNames from 'classnames'; import { Avatar, AvatarSize } from './Avatar'; import { Emojify } from './conversation/Emojify'; +import { useConversationUsername, useIsMe } from '../hooks/useParamSelector'; -interface Props { +type Props = { pubkey: string; - isMe?: boolean; - name?: string; - profileName?: string; - avatarPath?: string; onClick?: () => void; -} - -export class ContactListItem extends React.Component { - public renderAvatar() { - const { avatarPath, name, pubkey, profileName } = this.props; - - const userName = name || profileName || pubkey; - - return ; - } - - public render() { - const { name, onClick, isMe, pubkey, profileName } = this.props; - - const title = name ? name : pubkey; - const displayName = isMe ? window.i18n('me') : title; - - const profileElement = - !isMe && profileName && !name ? ( - - ~ - - - ) : null; - - return ( -
- {this.renderAvatar()} -
-
- {profileElement} -
+}; + +const AvatarItem = (props: { pubkey: string }) => { + const { pubkey } = props; + + return ; +}; + +export const ContactListItem = (props: Props) => { + const { onClick, pubkey } = props; + + const name = useConversationUsername(pubkey); + const isMe = useIsMe(pubkey); + + const title = name ? name : pubkey; + const displayName = isMe ? window.i18n('me') : title; + + return ( +
+ +
+
+
- ); - } -} +
+ ); +}; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index f8b8a37d1..cce539e08 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -26,6 +26,7 @@ import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; import { updateUserDetailsModal } from '../state/ducks/modalDialog'; +import { useAvatarPath, useConversationUsername, useIsMe } from '../hooks/useParamSelector'; // tslint:disable-next-line: no-empty-interface export interface ConversationListItemProps extends ReduxConversationType {} @@ -52,11 +53,8 @@ const Portal = ({ children }: { children: any }) => { const HeaderItem = (props: { unreadCount: number; - isMe: boolean; mentionedUs: boolean; activeAt?: number; - name?: string; - profileName?: string; conversationId: string; isPinned: boolean; currentNotificationSetting: ConversationNotificationSettingType; @@ -65,11 +63,8 @@ const HeaderItem = (props: { unreadCount, mentionedUs, activeAt, - isMe, isPinned, conversationId, - profileName, - name, currentNotificationSetting, } = props; @@ -116,12 +111,7 @@ const HeaderItem = (props: { unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null )} > - +
@@ -143,21 +133,18 @@ const HeaderItem = (props: { ); }; -const UserItem = (props: { - name?: string; - profileName?: string; - isMe: boolean; - conversationId: string; -}) => { - const { name, conversationId, profileName, isMe } = props; +const UserItem = (props: { conversationId: string }) => { + const { conversationId } = props; const shortenedPubkey = PubKey.shorten(conversationId); + const isMe = useIsMe(conversationId); + const username = useConversationUsername(conversationId); - const displayedPubkey = profileName ? shortenedPubkey : conversationId; - const displayName = isMe ? window.i18n('noteToSelf') : profileName; + const displayedPubkey = username ? shortenedPubkey : conversationId; + const displayName = isMe ? window.i18n('noteToSelf') : username; let shouldShowPubkey = false; - if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) { + if ((!username || username.length === 0) && (!displayName || displayName.length === 0)) { shouldShowPubkey = true; } @@ -165,7 +152,7 @@ const UserItem = (props: {
{ - const { avatarPath, name, isPrivate, conversationId, profileName } = props; - const userName = name || profileName || conversationId; +const AvatarItem = (props: { conversationId: string; isPrivate: boolean }) => { + const { isPrivate, conversationId } = props; + const userName = useConversationUsername(conversationId); + const avatarPath = useAvatarPath(conversationId); const dispatch = useDispatch(); return (
{ @@ -235,7 +215,7 @@ const AvatarItem = (props: { dispatch( updateUserDetailsModal({ conversationId: conversationId, - userName, + userName: userName || '', authorAvatarPath: avatarPath, }) ); @@ -256,9 +236,7 @@ const ConversationListItem = (props: Props) => { style, mentionedUs, isMe, - name, isPinned, - profileName, isTyping, lastMessage, hasNickname, @@ -309,23 +287,14 @@ const ConversationListItem = (props: Props) => { isBlocked ? 'module-conversation-list-item--is-blocked' : null )} > - +
{ type={type} currentNotificationSetting={currentNotificationSetting || 'all'} avatarPath={avatarPath || null} - name={name} - profileName={profileName} />
diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 5c2c3f1d1..d9a470820 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -162,26 +162,20 @@ const ExpirationLength = (props: { expirationSettingName?: string }) => { }; const AvatarHeader = (props: { - avatarPath: string | null; - name?: string; pubkey: string; - profileName?: string; showBackButton: boolean; onAvatarClick?: (pubkey: string) => void; }) => { - const { avatarPath, name, pubkey, profileName } = props; - const userName = name || profileName || pubkey; + const { pubkey, onAvatarClick, showBackButton } = props; return ( { // do not allow right panel to appear if another button is shown on the SessionConversation - if (props.onAvatarClick && !props.showBackButton) { - props.onAvatarClick(pubkey); + if (onAvatarClick && !showBackButton) { + onAvatarClick(pubkey); } }} pubkey={pubkey} @@ -393,9 +387,6 @@ export const ConversationHeaderWithDetails = () => { }} pubkey={conversationKey} showBackButton={isMessageDetailOpened} - avatarPath={avatarPath} - name={name} - profileName={profileName} /> )} diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 15e94b201..7080a6736 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -13,11 +13,10 @@ import { } from '../../state/selectors/conversations'; import { deleteMessagesById } from '../../interactions/conversations/unsendingInteractions'; -const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => { - const { avatarPath, pubkey, name, profileName } = props.contact; - const userName = name || profileName || pubkey; +const AvatarItem = (props: { pubkey: string | undefined }) => { + const { pubkey } = props; - return ; + return ; }; const DeleteButtonItem = (props: { messageId: string; convoId: string; isDeletable: boolean }) => { @@ -68,7 +67,7 @@ const ContactItem = (props: { contact: ContactPropsMessageDetail }) => { return (
- +
{ return (
- + {isPublic && isSenderAdmin && (
diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index 4c84a81ea..045e506cb 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -246,7 +246,12 @@ export class EditProfileDialog extends React.Component<{}, State> { const userName = profileName || this.convo.id; return ( - + ); } diff --git a/ts/components/dialog/UpdateGroupNameDialog.tsx b/ts/components/dialog/UpdateGroupNameDialog.tsx index 7409ef923..e96b2e612 100644 --- a/ts/components/dialog/UpdateGroupNameDialog.tsx +++ b/ts/components/dialog/UpdateGroupNameDialog.tsx @@ -191,7 +191,7 @@ export class UpdateGroupNameDialog extends React.Component { return (
- +
{ const convo = getConversationController().get(props.conversationId); const size = isEnlargedImageShown ? AvatarSize.HUGE : AvatarSize.XL; - const userName = props.userName || props.conversationId; const [_, copyToClipboard] = useCopyToClipboard(); @@ -57,8 +56,6 @@ export const UserDetailsDialog = (props: Props) => {
{ setIsEnlargedImageShown(!isEnlargedImageShown); diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 8e09ca7f9..7ad2f323b 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -50,11 +50,11 @@ import { DraggableCallContainer } from './calling/DraggableCallContainer'; import { IncomingCallDialog } from './calling/IncomingCallDialog'; import { CallInFullScreenContainer } from './calling/CallInFullScreenContainer'; -const Section = (props: { type: SectionType; avatarPath?: string | null }) => { +const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); const unreadMessageCount = useSelector(getUnreadMessageCount); const dispatch = useDispatch(); - const { type, avatarPath } = props; + const { type } = props; const focusedSection = useSelector(getFocusedSection); const isSelected = focusedSection === props.type; @@ -85,16 +85,10 @@ const Section = (props: { type: SectionType; avatarPath?: string | null }) => { }; if (type === SectionType.Profile) { - const conversation = getConversationController().get(ourNumber); - - const profile = conversation?.getLokiProfile(); - const userName = (profile && profile.displayName) || ourNumber; return ( @@ -287,12 +281,7 @@ export const ActionsPanel = () => { return () => clearTimeout(timeout); }, []); - useInterval( - () => { - cleanUpOldDecryptedMedias(); - }, - startCleanUpMedia ? cleanUpMediasInterval : null - ); + useInterval(cleanUpOldDecryptedMedias, startCleanUpMedia ? cleanUpMediasInterval : null); if (!ourPrimaryConversation) { window?.log?.warn('ActionsPanel: ourPrimaryConversation is not set'); @@ -328,7 +317,7 @@ export const ActionsPanel = () => { className="module-left-pane__sections-container" data-testid="leftpane-section-container" > -
+
diff --git a/ts/components/session/SessionMemberListItem.tsx b/ts/components/session/SessionMemberListItem.tsx index 9cf9fd0b6..f90a03f19 100644 --- a/ts/components/session/SessionMemberListItem.tsx +++ b/ts/components/session/SessionMemberListItem.tsx @@ -23,37 +23,17 @@ type Props = { isSelected: boolean; // this bool is used to make a zombie appear with less opacity than a normal member isZombie?: boolean; - onSelect?: any; - onUnselect?: any; + onSelect?: (selectedMember: ContactType) => void; + onUnselect?: (selectedMember: ContactType) => void; +}; + +const AvatarItem = (props: { memberPubkey?: string }) => { + return ; }; export const SessionMemberListItem = (props: Props) => { const { isSelected, member, isZombie, onSelect, onUnselect } = props; - const renderAvatar = () => { - const { authorAvatarPath, authorName, authorPhoneNumber, authorProfileName } = member; - const userName = authorName || authorProfileName || authorPhoneNumber; - return ( - - ); - }; - - const selectMember = () => { - onSelect?.(member); - }; - const unselectMember = () => { - onUnselect?.(member); - }; - - const handleSelectionAction = () => { - isSelected ? unselectMember() : selectMember(); - }; - const name = member.authorProfileName || PubKey.shorten(member.authorPhoneNumber); return ( @@ -64,11 +44,15 @@ export const SessionMemberListItem = (props: Props) => { isSelected && 'selected', isZombie && 'zombie' )} - onClick={handleSelectionAction} + onClick={() => { + isSelected ? onUnselect?.(member) : onSelect?.(member); + }} role="button" >
- {renderAvatar()} + + + {name}
diff --git a/ts/components/session/calling/DraggableCallContainer.tsx b/ts/components/session/calling/DraggableCallContainer.tsx index 7455da648..04b0e9194 100644 --- a/ts/components/session/calling/DraggableCallContainer.tsx +++ b/ts/components/session/calling/DraggableCallContainer.tsx @@ -9,7 +9,6 @@ import { getHasOngoingCall, getHasOngoingCallWith } from '../../../state/selecto import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { Avatar, AvatarSize } from '../../Avatar'; import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; -import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector'; import { VideoLoadingSpinner } from './InConversationCallContainer'; export const DraggableCallWindow = styled.div` @@ -77,8 +76,6 @@ export const DraggableCallContainer = () => { 'DraggableCallContainer', false ); - const ongoingCallUsername = useConversationUsername(ongoingCallPubkey); - const avatarPath = useAvatarPath(ongoingCallPubkey); const videoRefRemote = useRef(null); function onWindowResize() { @@ -140,12 +137,7 @@ export const DraggableCallContainer = () => { /> {remoteStreamVideoIsMuted && ( - + )} diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index 85d0384f3..87fa01175 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -6,7 +6,6 @@ import _ from 'underscore'; import { UserUtils } from '../../../session/utils'; import { getCallIsInFullScreen, - getHasOngoingCallWith, getHasOngoingCallWithFocusedConvo, getHasOngoingCallWithFocusedConvoIsOffering, getHasOngoingCallWithFocusedConvosIsConnecting, @@ -16,11 +15,6 @@ import { StyledVideoElement } from './DraggableCallContainer'; import { Avatar, AvatarSize } from '../../Avatar'; import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; -import { - useAvatarPath, - useOurAvatarPath, - useOurConversationUsername, -} from '../../../hooks/useParamSelector'; import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots'; import { CallWindowControls } from './CallButtons'; import { SessionSpinner } from '../SessionSpinner'; @@ -118,23 +112,15 @@ export const VideoLoadingSpinner = (props: { fullWidth: boolean }) => { // tslint:disable-next-line: max-func-body-length export const InConversationCallContainer = () => { - const ongoingCallProps = useSelector(getHasOngoingCallWith); - const isInFullScreen = useSelector(getCallIsInFullScreen); const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo); - const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name; const videoRefRemote = useRef(null); const videoRefLocal = useRef(null); const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); - const remoteAvatarPath = useAvatarPath(ongoingCallPubkey); - const ourAvatarPath = useOurAvatarPath(); - - const ourUsername = useOurConversationUsername(); - const { currentConnectedAudioInputs, currentConnectedCameras, @@ -190,12 +176,7 @@ export const InConversationCallContainer = () => { /> {remoteStreamVideoIsMuted && ( - + )} @@ -208,12 +189,7 @@ export const InConversationCallContainer = () => { /> {localStreamVideoIsMuted && ( - + )} diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx index d022e2c26..3e6dd5500 100644 --- a/ts/components/session/calling/IncomingCallDialog.tsx +++ b/ts/components/session/calling/IncomingCallDialog.tsx @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import _ from 'underscore'; -import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector'; +import { useConversationUsername } from '../../../hooks/useParamSelector'; import { ed25519Str } from '../../../session/onions/onionPath'; import { CallManager } from '../../../session/utils'; import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call'; @@ -70,7 +70,6 @@ export const IncomingCallDialog = () => { } }; const from = useConversationUsername(incomingCallFromPubkey); - const incomingAvatar = useAvatarPath(incomingCallFromPubkey); if (!hasIncomingCall) { return null; } @@ -79,12 +78,7 @@ export const IncomingCallDialog = () => { return ( - +
{ if (!selectedConversation) { return null; } - const { - avatarPath, - id, - isGroup, - isKickedFromGroup, - profileName, - isBlocked, - left, - name, - } = selectedConversation; + const { id, isGroup, isKickedFromGroup, isBlocked, left } = selectedConversation; const showInviteContacts = isGroup && !isKickedFromGroup && !isBlocked && !left; - const userName = name || profileName || id; return (
@@ -137,7 +127,7 @@ const HeaderItem = () => { dispatch(closeRightPanel()); }} /> - +
{showInviteContacts && ( isKickedFromGroup, currentNotificationSetting, isPrivate, - name, - profileName, - avatarPath, } = props; const isGroup = type === 'group'; - const userName = name || profileName || conversationId; + + const userName = useConversationUsername(conversationId); + const avatarPath = useAvatarPath(conversationId); return ( @@ -80,7 +78,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => {getInviteContactMenuItem(isGroup, isPublic, conversationId)} {getDeleteContactMenuItem(isGroup, isPublic, left, isKickedFromGroup, conversationId)} {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} - {getShowUserDetailsMenuItem(isPrivate, conversationId, avatarPath, userName)} + {getShowUserDetailsMenuItem(isPrivate, conversationId, avatarPath, userName || '')} ); }; diff --git a/ts/hooks/useMembersAvatars.tsx b/ts/hooks/useMembersAvatars.tsx index 39e972a21..8f80c6616 100644 --- a/ts/hooks/useMembersAvatars.tsx +++ b/ts/hooks/useMembersAvatars.tsx @@ -3,16 +3,10 @@ import * as _ from 'lodash'; import { useSelector } from 'react-redux'; import { StateType } from '../state/reducer'; -export type ConversationAvatar = { - avatarPath?: string; - id: string; // member's pubkey - name: string; -}; - export function useMembersAvatars(closedGroupPubkey: string | undefined) { const ourPrimary = UserUtils.getOurPubKeyStrFromCache(); - return useSelector((state: StateType): Array | undefined => { + return useSelector((state: StateType): Array | undefined => { if (!closedGroupPubkey) { return undefined; } @@ -37,16 +31,7 @@ export function useMembersAvatars(closedGroupPubkey: string | undefined) { usAtTheEndMaxTwo .map(m => state.conversations.conversationLookup[m]) .map(m => { - if (!m) { - return undefined; - } - const userName = m.name || m.profileName || m.id; - - return { - avatarPath: m.avatarPath || undefined, - id: m.id, - name: userName, - }; + return m?.id || undefined; }) ); diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index 595727dc9..e9ab569d4 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -5,9 +5,9 @@ import { StateType } from '../state/reducer'; export function useAvatarPath(pubkey: string | undefined) { return useSelector((state: StateType) => { if (!pubkey) { - return undefined; + return null; } - return state.conversations.conversationLookup[pubkey]?.avatarPath; + return state.conversations.conversationLookup[pubkey]?.avatarPath || null; }); } @@ -15,12 +15,19 @@ export function useOurAvatarPath() { return useAvatarPath(UserUtils.getOurPubKeyStrFromCache()); } -export function useConversationUsername(pubkey: string | undefined) { +/** + * + * @returns convo.profileName || convo.name || convo.id or undefined if the convo is not found + */ +export function useConversationUsername(pubkey?: string) { return useSelector((state: StateType) => { if (!pubkey) { return undefined; } const convo = state.conversations.conversationLookup[pubkey]; + if (!convo) { + return pubkey; + } return convo?.profileName || convo?.name || convo.id; }); } @@ -28,3 +35,7 @@ export function useConversationUsername(pubkey: string | undefined) { export function useOurConversationUsername() { return useConversationUsername(UserUtils.getOurPubKeyStrFromCache()); } + +export function useIsMe(pubkey?: string) { + return pubkey && pubkey === UserUtils.getOurPubKeyStrFromCache(); +} From faeb6e206ad1274d711122aa8bcd5c1b0f5098f5 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 16:18:52 +1100 Subject: [PATCH 46/70] fix a bug releasing the decrypted attachment blobs too early --- ts/session/crypto/DecryptedAttachmentsManager.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ts/session/crypto/DecryptedAttachmentsManager.ts b/ts/session/crypto/DecryptedAttachmentsManager.ts index 5e578d291..54b22b390 100644 --- a/ts/session/crypto/DecryptedAttachmentsManager.ts +++ b/ts/session/crypto/DecryptedAttachmentsManager.ts @@ -32,8 +32,6 @@ export const cleanUpOldDecryptedMedias = () => { countKept++; } } - urlToDecryptedBlobMap.clear(); - urlToDecryptingPromise.clear(); window?.log?.info(`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`); }; From 726418887cb0b0537f49b2cdc1a42a6d1531be0d Mon Sep 17 00:00:00 2001 From: warrickct Date: Wed, 24 Nov 2021 09:32:07 +1100 Subject: [PATCH 47/70] Addressing PR comments --- ts/components/ConversationListItem.tsx | 8 ++- .../session/LeftPaneMessageSection.tsx | 50 +++++++++++-------- ts/receiver/configMessage.ts | 14 +++++- .../conversations/ConversationController.ts | 6 --- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 4cd52f7e8..9725cc20b 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -377,15 +377,13 @@ const ConversationListItem = (props: Props) => { - Block - + text={window.i18n('blockUser')} + /> + /> ) : null}
diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index c12ea5843..2632409fd 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -221,6 +221,33 @@ export class LeftPaneMessageSection extends React.Component { this.handleToggleOverlay(SessionClosableOverlayType.MessageRequests); } + /** + * Blocks all message request conversations and synchronizes across linked devices + * @returns void + */ + private async handleBlockAllRequestsClick() { + // block all convo requests. Force sync if there were changes. + window?.log?.info('Blocking all conversations'); + const { conversationRequests } = this.props; + let syncRequired = false; + + if (!conversationRequests) { + window?.log?.info('No conversation requests to block.'); + return; + } + + await Promise.all( + conversationRequests.map(async convo => { + await BlockedNumberController.block(convo.id); + syncRequired = true; + }) + ); + + if (syncRequired) { + await forceSyncConfigurationNowIfNeeded(); + } + } + private renderClosableOverlay() { const { searchTerm, searchResults } = this.props; const { loading, overlay } = this.state; @@ -278,28 +305,7 @@ export class LeftPaneMessageSection extends React.Component { onCloseClick={() => { this.handleToggleOverlay(undefined); }} - onButtonClick={async () => { - // block all convo requests. Force sync if there were changes. - window?.log?.info('Blocking all conversations'); - const { conversationRequests } = this.props; - let syncRequired = false; - - if (!conversationRequests) { - window?.log?.info('No conversation requests to block.'); - return; - } - - await Promise.all( - conversationRequests.map(async convo => { - await BlockedNumberController.block(convo.id); - syncRequired = true; - }) - ); - - if (syncRequired) { - await forceSyncConfigurationNowIfNeeded(); - } - }} + onButtonClick={this.handleBlockAllRequestsClick} searchTerm={searchTerm} searchResults={searchResults} showSpinner={loading} diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 86a0de634..362355306 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { createOrUpdateItem } from '../data/data'; +import { createOrUpdateItem, getItemById, hasSyncedInitialConfigurationItem } from '../data/data'; import { ConversationTypeEnum } from '../models/conversation'; import { joinOpenGroupV2WithUIEvents, @@ -59,6 +59,18 @@ async function handleGroupsAndContactsFromConfigMessage( value: true, }); + const didWeHandleAConfigurationMessageAlready = + (await getItemById(hasSyncedInitialConfigurationItem))?.value || false; + if (didWeHandleAConfigurationMessageAlready) { + window?.log?.info( + 'Dropping configuration groups change as we already handled one... Only handling contacts ' + ); + if (configMessage.contacts?.length) { + await Promise.all(configMessage.contacts.map(async c => handleContactReceived(c, envelope))); + } + return; + } + const numberClosedGroup = configMessage.closedGroups?.length || 0; window?.log?.info( diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 629851540..0bb29e58c 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -266,12 +266,6 @@ export class ConversationController { return Array.from(this.conversations.models); } - public getConversationRequests(): Array { - return Array.from(this.conversations.models).filter( - conversation => conversation.isApproved() && !conversation.isBlocked - ); - } - public unsafeDelete(convo: ConversationModel) { this.conversations.remove(convo); } From 9823a700e278aa30d85b5c01662063fef0e0b179 Mon Sep 17 00:00:00 2001 From: warrickct Date: Wed, 24 Nov 2021 11:14:24 +1100 Subject: [PATCH 48/70] Addressing PR fixes --- preload.js | 5 ---- ts/components/ConversationListItem.tsx | 26 +++++++------------ .../session/LeftPaneMessageSection.tsx | 9 ++----- .../session/MessageRequestsBanner.tsx | 2 +- .../session/SessionClosableOverlay.tsx | 2 +- .../settings/section/CategoryPrivacy.tsx | 10 +++---- ts/interactions/conversationInteractions.ts | 11 ++++++++ ts/state/selectors/userConfig.ts | 5 ++++ 8 files changed, 34 insertions(+), 36 deletions(-) diff --git a/preload.js b/preload.js index cd1679c43..ebe50c750 100644 --- a/preload.js +++ b/preload.js @@ -39,11 +39,6 @@ window.isBehindProxy = () => Boolean(config.proxyUrl); window.lokiFeatureFlags = { useOnionRequests: true, - useFileOnionRequests: true, - useFileOnionRequestsV2: true, // more compact encoding of files in response - padOutgoingAttachments: true, - enablePinConversations: true, - useUnsendRequests: false, useMessageRequests: true, useCallMessage: true, }; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 9725cc20b..4d0ea641a 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -29,10 +29,9 @@ import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; import { Flex } from './basic/Flex'; import { SessionButton, SessionButtonColor } from './session/SessionButton'; -import { getConversationById } from '../data/data'; import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; -import { BlockedNumberController } from '../util'; import { updateUserDetailsModal } from '../state/ducks/modalDialog'; +import { approveConversation, blockConvoById } from '../interactions/conversationInteractions'; // tslint:disable-next-line: no-empty-interface export interface ConversationListItemProps extends ReduxConversationType {} @@ -303,20 +302,7 @@ const ConversationListItem = (props: Props) => { * adds ID to block list, syncs the block with linked devices. */ const handleConversationBlock = async () => { - const convoToBlock = await getConversationById(conversationId); - if (!convoToBlock) { - window?.log?.error('Unable to find conversation to be blocked.'); - } - await BlockedNumberController.block(convoToBlock?.id); - await forceSyncConfigurationNowIfNeeded(); - }; - - /** - * marks the conversation as approved. - */ - const handleConversationAccept = async () => { - const conversationToApprove = await getConversationById(conversationId); - await conversationToApprove?.setIsApproved(true); + blockConvoById(conversationId); await forceSyncConfigurationNowIfNeeded(); }; @@ -325,6 +311,10 @@ const ConversationListItem = (props: Props) => {
{ + e.stopPropagation(); + e.preventDefault(); + }} onContextMenu={(e: any) => { contextMenu.show({ id: triggerId, @@ -381,7 +371,9 @@ const ConversationListItem = (props: Props) => { /> { + approveConversation(conversationId); + }} text={window.i18n('accept')} /> diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 2632409fd..694e46612 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -107,7 +107,7 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('render: must provided conversations if no search results are provided'); } - const length = this.props.conversations ? this.props.conversations.length : 0; + const length = conversations.length; const listKey = 0; // Note: conversations is not a known prop for List, but it is required to ensure that @@ -119,7 +119,7 @@ export class LeftPaneMessageSection extends React.Component { {({ height, width }) => ( { const messageRequestsElement = ( { this.handleToggleOverlay(undefined); }} onButtonClick={this.handleBlockAllRequestsClick} - searchTerm={searchTerm} - searchResults={searchResults} - showSpinner={loading} - updateSearch={this.updateSearch} /> ); diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index 7e2f5408e..ffd5a970b 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -92,7 +92,7 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { return ( - Message Requests + {window.i18n('messageRequests')}
{conversationRequests.length || 0}
diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index a533d09a2..14d6efd72 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -25,7 +25,7 @@ export enum SessionClosableOverlayType { interface Props { overlayMode: SessionClosableOverlayType; - onChangeSessionID: any; + onChangeSessionID?: any; onCloseClick: any; onButtonClick: any; contacts?: Array; diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx index da4e7c02f..e876bbc2b 100644 --- a/ts/components/session/settings/section/CategoryPrivacy.tsx +++ b/ts/components/session/settings/section/CategoryPrivacy.tsx @@ -1,8 +1,10 @@ import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; // tslint:disable-next-line: no-submodule-imports import useUpdate from 'react-use/lib/useUpdate'; import { sessionPassword, updateConfirmModal } from '../../../../state/ducks/modalDialog'; import { toggleMessageRequests } from '../../../../state/ducks/userConfig'; +import { getIsMessageRequestsEnabled } from '../../../../state/selectors/userConfig'; import { PasswordAction } from '../../../dialog/SessionPasswordDialog'; import { SessionButtonColor } from '../../SessionButton'; import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem'; @@ -53,6 +55,7 @@ export const SettingsCategoryPrivacy = (props: { onPasswordUpdated: (action: string) => void; }) => { const forceUpdate = useUpdate(); + const dispatch = useDispatch(); if (props.hasPassword !== null) { return ( @@ -110,14 +113,11 @@ export const SettingsCategoryPrivacy = (props: { /> { - // const old = Boolean(window.getSettingValue(settingsAutoUpdate)); - // window.setSettingValue(settingsAutoUpdate, !old); - window.inboxStore?.dispatch(toggleMessageRequests()); - forceUpdate(); + dispatch(toggleMessageRequests()); }} title={window.i18n('messageRequests')} description={window.i18n('messageRequestsDescription')} - active={Boolean(window.getSettingValue(settingsAutoUpdate))} + active={useSelector(getIsMessageRequestsEnabled)} /> {!props.hasPassword && ( { if (convoId.match(openGroupV2ConversationIdRegex)) { @@ -115,6 +117,15 @@ export async function unblockConvoById(conversationId: string) { await conversation.commit(); } +/** + * marks the conversation as approved. + */ +export const approveConversation = async (conversationId: string) => { + const conversationToApprove = await getConversationById(conversationId); + await conversationToApprove?.setIsApproved(true); + await forceSyncConfigurationNowIfNeeded(); +}; + export async function showUpdateGroupNameByConvoId(conversationId: string) { const conversation = getConversationController().get(conversationId); if (conversation.isMediumGroup()) { diff --git a/ts/state/selectors/userConfig.ts b/ts/state/selectors/userConfig.ts index 9c8641cb2..39dd45eba 100644 --- a/ts/state/selectors/userConfig.ts +++ b/ts/state/selectors/userConfig.ts @@ -13,3 +13,8 @@ export const getShowRecoveryPhrasePrompt = createSelector( getUserConfig, (state: UserConfigState): boolean => state.showRecoveryPhrasePrompt ); + +export const getIsMessageRequestsEnabled = createSelector( + getUserConfig, + (state: UserConfigState): boolean => state.messageRequests +); From b5df47c2b892950d716973feda5e32edc5e89fd3 Mon Sep 17 00:00:00 2001 From: warrickct Date: Wed, 24 Nov 2021 13:26:04 +1100 Subject: [PATCH 49/70] Addressing PR comments --- ts/components/ConversationListItem.tsx | 6 +-- ts/components/LeftPane.tsx | 4 +- .../session/LeftPaneMessageSection.tsx | 41 ++++++++++++++++++- .../session/MessageRequestsBanner.tsx | 4 +- ts/models/conversation.ts | 12 +++--- ts/receiver/configMessage.ts | 6 +-- .../conversations/ConversationController.ts | 1 - 7 files changed, 55 insertions(+), 19 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 4d0ea641a..463a90a88 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -302,7 +302,7 @@ const ConversationListItem = (props: Props) => { * adds ID to block list, syncs the block with linked devices. */ const handleConversationBlock = async () => { - blockConvoById(conversationId); + await blockConvoById(conversationId); await forceSyncConfigurationNowIfNeeded(); }; @@ -371,8 +371,8 @@ const ConversationListItem = (props: Props) => { /> { - approveConversation(conversationId); + onClick={async () => { + await approveConversation(conversationId); }} text={window.i18n('accept')} /> diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 81beaef98..19ca70545 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -8,7 +8,7 @@ import { LeftPaneSettingSection } from './session/LeftPaneSettingSection'; import { SessionTheme } from '../state/ducks/SessionTheme'; import { getFocusedSection } from '../state/selectors/section'; import { useSelector } from 'react-redux'; -import { getConversationRequests, getLeftPaneLists } from '../state/selectors/conversations'; +import { getLeftPaneLists } from '../state/selectors/conversations'; import { getQuery, getSearchResults, isSearching } from '../state/selectors/search'; import { SectionType } from '../state/ducks/section'; @@ -29,14 +29,12 @@ const InnerLeftPaneMessageSection = () => { const searchResults = showSearch ? useSelector(getSearchResults) : undefined; const lists = showSearch ? undefined : useSelector(getLeftPaneLists); - const conversationRequests = useSelector(getConversationRequests); // tslint:disable: use-simple-attributes return ( diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 694e46612..848651b30 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -37,7 +37,6 @@ export interface Props { contacts: Array; conversations?: Array; - conversationRequests?: Array; searchResults?: SearchResultsProps; } @@ -226,9 +225,47 @@ export class LeftPaneMessageSection extends React.Component { * @returns void */ private async handleBlockAllRequestsClick() { + let messageRequestsEnabled = false; + if (window?.inboxStore?.getState()) { + messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true && + window.lokiFeatureFlags?.useMessageRequests === true; + } + if (!messageRequestsEnabled) { + return; + } + // block all convo requests. Force sync if there were changes. window?.log?.info('Blocking all conversations'); - const { conversationRequests } = this.props; + const conversations = getConversationController().getConversations(); + + if (!conversations) { + window?.log?.info('No message requests to block.'); + return; + } + + const conversationRequests = conversations.filter(conversation => { + // Add Open Group to list as soon as the name has been set + if ( + conversation.isPublic() && + (!conversation.get('name') || conversation.get('name') === 'Unknown group') + ) { + return false; + } + + // Remove all invalid conversations and conversatons of devices associated + // with cancelled attempted links + if (!conversation.isPublic && !conversation.get('active_at')) { + return false; + } + + if (conversation.attributes.isApproved || !conversation.get('active_at')) { + return false; + } + + return true; + }); + let syncRequired = false; if (!conversationRequests) { diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index ffd5a970b..e19642439 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -92,7 +92,9 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { return ( - {window.i18n('messageRequests')} + + {window.i18n('messageRequests')} +
{conversationRequests.length || 0}
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 3e6fe5e7b..7c1edb278 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1042,7 +1042,12 @@ export class ConversationModel extends Backbone.Model { const model = new MessageModel(messageAttributes); const isMe = messageAttributes.source === UserUtils.getOurPubKeyStrFromCache(); - if (isMe) { + + if ( + isMe && + window.lokiFeatureFlags.useMessageRequests && + window.inboxStore?.getState().userConfig.messageRequests + ) { await this.setIsApproved(true); } @@ -1288,11 +1293,6 @@ export class ConversationModel extends Backbone.Model { isApproved: value, }); - // to exclude the conversation from left pane messages list and message requests - if (value === false) { - this.set({ active_at: undefined }); - } - await this.commit(); } } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 362355306..b091343b3 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -138,14 +138,14 @@ const handleContactReceived = async ( contactConvo.set('active_at', _.toNumber(envelope.timestamp)); if ( - window.lokiFeatureFlags.useMessageRequests === true && + window.lokiFeatureFlags.useMessageRequests && window.inboxStore?.getState().userConfig.messageRequests ) { - if (contactReceived.isApproved === true) { + if (contactReceived.isApproved) { await contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); } - if (contactReceived.isBlocked === true) { + if (contactReceived.isBlocked) { await BlockedNumberController.block(contactConvo.id); } else { await BlockedNumberController.unblock(contactConvo.id); diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 0bb29e58c..1de41ac0f 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -236,7 +236,6 @@ export class ConversationController { if (conversation.isPrivate()) { window.log.info(`deleteContact isPrivate, marking as inactive: ${id}`); - // conversation.set('active_at', undefined); conversation.set({ active_at: undefined, isApproved: false, From f91e2c4edd96ac28e3e7bfe2caaddf37d4c40cd3 Mon Sep 17 00:00:00 2001 From: warrickct Date: Wed, 24 Nov 2021 13:55:16 +1100 Subject: [PATCH 50/70] Minor PR fixes --- ts/components/session/LeftPaneMessageSection.tsx | 7 ++----- ts/receiver/callMessage.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 848651b30..4c2c56b00 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -82,12 +82,9 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('renderRow: Tried to render without conversations'); } - let conversation; - if (conversations?.length) { - conversation = conversations[index]; - } - + const conversation = conversations[index]; if (!conversation) { + window?.log?.info('No conversation found at index'); return null; } diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index bec8f2f6b..3670670fb 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import { SignalService } from '../protobuf'; import { TTL_DEFAULT } from '../session/constants'; import { SNodeAPI } from '../session/snode_api'; -import { CallManager, UserUtils } from '../session/utils'; +import { CallManager } from '../session/utils'; import { removeFromCache } from './cache'; import { EnvelopePlus } from './types'; From c3e58f725e0478b7190e32644a83f666da8778ca Mon Sep 17 00:00:00 2001 From: warrickct Date: Wed, 24 Nov 2021 15:37:22 +1100 Subject: [PATCH 51/70] Adding trigger logic for conversation filtering of requests. --- ts/state/selectors/conversations.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index ed0022ab4..44ebe7620 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -36,6 +36,7 @@ import { MessageAttachmentSelectorProps } from '../../components/conversation/me import { MessageContentSelectorProps } from '../../components/conversation/message/MessageContent'; import { MessageContentWithStatusSelectorProps } from '../../components/conversation/message/MessageContentWithStatus'; import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/GenericReadableMessage'; +import { getIsMessageRequestsEnabled } from './userConfig'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -425,6 +426,7 @@ export const getConversationComparator = createSelector(getIntl, _getConversatio export const _getLeftPaneLists = ( lookup: ConversationLookupType, comparator: (left: ReduxConversationType, right: ReduxConversationType) => number, + isMessageRequestEnabled?: boolean, selectedConversation?: string ): { conversations: Array; @@ -456,13 +458,8 @@ export const _getLeftPaneLists = ( }; } - let messageRequestsEnabled = false; - - if (window?.inboxStore?.getState()) { - messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true && - window.lokiFeatureFlags?.useMessageRequests === true; - } + const excludeUnapproved = + isMessageRequestEnabled && window.lokiFeatureFlags?.useMessageRequests; // Add Open Group to list as soon as the name has been set if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { @@ -479,7 +476,7 @@ export const _getLeftPaneLists = ( directConversations.push(conversation); } - if (messageRequestsEnabled && !conversation.isApproved && !conversation.isBlocked) { + if (excludeUnapproved && !conversation.isApproved && !conversation.isBlocked) { // dont increase unread counter, don't push to convo list. continue; } @@ -571,6 +568,7 @@ export const getConversationRequests = createSelector( export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, + getIsMessageRequestsEnabled, getSelectedConversationKey, _getLeftPaneLists ); From e32f20d8bc04cec3abe52f1d4db84f163bbd8010 Mon Sep 17 00:00:00 2001 From: warrickct Date: Fri, 26 Nov 2021 13:20:03 +1100 Subject: [PATCH 52/70] PR changes --- ts/components/LeftPane.tsx | 4 +- .../session/LeftPaneMessageSection.tsx | 13 ++- .../session/SessionClosableOverlay.tsx | 18 ++-- ts/interactions/conversationInteractions.ts | 11 ++- ts/models/conversation.ts | 2 +- ts/receiver/callMessage.ts | 2 +- ts/receiver/configMessage.ts | 12 +-- ts/state/selectors/conversations.ts | 82 ++++++------------- .../unit/selectors/conversations_test.ts | 10 +-- 9 files changed, 66 insertions(+), 88 deletions(-) diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 19ca70545..194f00a29 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -11,6 +11,7 @@ import { useSelector } from 'react-redux'; import { getLeftPaneLists } from '../state/selectors/conversations'; import { getQuery, getSearchResults, isSearching } from '../state/selectors/search'; import { SectionType } from '../state/ducks/section'; +import { getIsMessageRequestsEnabled } from '../state/selectors/userConfig'; // from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5 export type RowRendererParamsType = { @@ -29,14 +30,15 @@ const InnerLeftPaneMessageSection = () => { const searchResults = showSearch ? useSelector(getSearchResults) : undefined; const lists = showSearch ? undefined : useSelector(getLeftPaneLists); + const messageRequestsEnabled = useSelector(getIsMessageRequestsEnabled); - // tslint:disable: use-simple-attributes return ( ); }; diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 4c2c56b00..6c3746d4b 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -38,6 +38,8 @@ export interface Props { contacts: Array; conversations?: Array; searchResults?: SearchResultsProps; + + messageRequestsEnabled?: boolean; } export enum SessionComposeToType { @@ -84,7 +86,7 @@ export class LeftPaneMessageSection extends React.Component { const conversation = conversations[index]; if (!conversation) { - window?.log?.info('No conversation found at index'); + throw new Error('renderRow: conversations selector returned element containing falsy value.'); return null; } @@ -222,12 +224,9 @@ export class LeftPaneMessageSection extends React.Component { * @returns void */ private async handleBlockAllRequestsClick() { - let messageRequestsEnabled = false; - if (window?.inboxStore?.getState()) { - messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true && - window.lokiFeatureFlags?.useMessageRequests === true; - } + const messageRequestsEnabled = + this.props.messageRequestsEnabled && window?.lokiFeatureFlags?.useMessageRequests; + if (!messageRequestsEnabled) { return; } diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 14d6efd72..967dc1b77 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -11,10 +11,7 @@ import { SessionJoinableRooms } from './SessionJoinableDefaultRooms'; import { SpacerLG, SpacerMD } from '../basic/Text'; import { useSelector } from 'react-redux'; import { getConversationRequests } from '../../state/selectors/conversations'; -import { - ConversationListItemProps, - MemoConversationListItemWithDetails, -} from '../ConversationListItem'; +import { MemoConversationListItemWithDetails } from '../ConversationListItem'; export enum SessionClosableOverlayType { Message = 'message', @@ -299,13 +296,14 @@ const MessageRequestList = () => { return (
{conversationRequests.map(conversation => { - return ; + return ( + + ); })}
); }; - -const MessageRequestListItem = (props: { conversation: ConversationListItemProps }) => { - const { conversation } = props; - return ; -}; diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 247cb3537..fad5af236 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -122,8 +122,17 @@ export async function unblockConvoById(conversationId: string) { */ export const approveConversation = async (conversationId: string) => { const conversationToApprove = await getConversationById(conversationId); + + if (!conversationToApprove || conversationToApprove.isApproved()) { + window?.log?.info('Conversation is already approved.'); + return; + } + await conversationToApprove?.setIsApproved(true); - await forceSyncConfigurationNowIfNeeded(); + + if (conversationToApprove?.isApproved() === true) { + await forceSyncConfigurationNowIfNeeded(); + } }; export async function showUpdateGroupNameByConvoId(conversationId: string) { diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 507b0f1d5..7a95a9979 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -738,7 +738,7 @@ export class ConversationModel extends Backbone.Model { !this.isApproved() && (this.isPrivate() || this.isMediumGroup() || this.isClosedGroup()); if (updateApprovalNeeded) { await this.setIsApproved(true); - await forceSyncConfigurationNowIfNeeded(); + void forceSyncConfigurationNowIfNeeded(); } if (this.isOpenGroupV2()) { diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index b1f387262..cbf0d4891 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import { SignalService } from '../protobuf'; import { TTL_DEFAULT } from '../session/constants'; import { SNodeAPI } from '../session/snode_api'; -import { CallManager } from '../session/utils'; +import { CallManager, UserUtils } from '../session/utils'; import { removeFromCache } from './cache'; import { EnvelopePlus } from './types'; diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index b091343b3..e396d76a1 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -54,11 +54,6 @@ async function handleGroupsAndContactsFromConfigMessage( envelope: EnvelopePlus, configMessage: SignalService.ConfigurationMessage ) { - await createOrUpdateItem({ - id: 'hasSyncedInitialConfigurationItem', - value: true, - }); - const didWeHandleAConfigurationMessageAlready = (await getItemById(hasSyncedInitialConfigurationItem))?.value || false; if (didWeHandleAConfigurationMessageAlready) { @@ -71,6 +66,11 @@ async function handleGroupsAndContactsFromConfigMessage( return; } + await createOrUpdateItem({ + id: 'hasSyncedInitialConfigurationItem', + value: true, + }); + const numberClosedGroup = configMessage.closedGroups?.length || 0; window?.log?.info( @@ -152,7 +152,7 @@ const handleContactReceived = async ( } } - await updateProfileOneAtATime(contactConvo, profile, contactReceived.profileKey); + void updateProfileOneAtATime(contactConvo, profile, contactReceived.profileKey); } catch (e) { window?.log?.warn('failed to handle a new closed group from configuration message'); } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index d508f1fbf..07927f2fd 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -331,54 +331,21 @@ export const getConversationComparator = createSelector(getIntl, _getConversatio // export only because we use it in some of our tests // tslint:disable-next-line: cyclomatic-complexity export const _getLeftPaneLists = ( - lookup: ConversationLookupType, - comparator: (left: ReduxConversationType, right: ReduxConversationType) => number, - isMessageRequestEnabled?: boolean, - selectedConversation?: string + sortedConversations: Array, + isMessageRequestEnabled?: boolean ): { conversations: Array; contacts: Array; unreadCount: number; } => { - const values = Object.values(lookup); - const sorted = values.sort(comparator); - const conversations: Array = []; const directConversations: Array = []; let unreadCount = 0; - for (let conversation of sorted) { - if (selectedConversation === conversation.id) { - conversation = { - ...conversation, - isSelected: true, - }; - } - const isBlocked = - BlockedNumberController.isBlocked(conversation.id) || - BlockedNumberController.isGroupBlocked(conversation.id); - - if (isBlocked) { - conversation = { - ...conversation, - isBlocked: true, - }; - } - + for (const conversation of sortedConversations) { const excludeUnapproved = isMessageRequestEnabled && window.lokiFeatureFlags?.useMessageRequests; - // Add Open Group to list as soon as the name has been set - if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { - continue; - } - - // Remove all invalid conversations and conversatons of devices associated - // with cancelled attempted links - if (!conversation.isPublic && !conversation.activeAt) { - continue; - } - if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) { directConversations.push(conversation); } @@ -407,7 +374,7 @@ export const _getLeftPaneLists = ( }; }; -export const _getConversationRequests = ( +export const _getSortedConversations = ( lookup: ConversationLookupType, comparator: (left: ReduxConversationType, right: ReduxConversationType) => number, selectedConversation?: string @@ -415,7 +382,7 @@ export const _getConversationRequests = ( const values = Object.values(lookup); const sorted = values.sort(comparator); - const conversationRequests: Array = []; + const sortedConversations: Array = []; for (let conversation of sorted) { if (selectedConversation === conversation.id) { @@ -436,14 +403,6 @@ export const _getConversationRequests = ( }; } - let messageRequestsEnabled = false; - - if (window?.inboxStore?.getState()) { - messageRequestsEnabled = - window.inboxStore?.getState().userConfig.messageRequests === true && - window.lokiFeatureFlags?.useMessageRequests === true; - } - // Add Open Group to list as soon as the name has been set if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) { continue; @@ -455,28 +414,39 @@ export const _getConversationRequests = ( continue; } - if (messageRequestsEnabled && !conversation.isApproved && !conversation.isBlocked) { - // dont increase unread counter, don't push to convo list. - conversationRequests.push(conversation); - continue; - } + sortedConversations.push(conversation); } - return conversationRequests; + return sortedConversations; }; -export const getConversationRequests = createSelector( +export const getSortedConversations = createSelector( getConversationLookup, getConversationComparator, getSelectedConversationKey, + _getSortedConversations +); + +export const _getConversationRequests = ( + sortedConversations: Array, + isMessageRequestEnabled?: boolean +): Array => { + const pushToMessageRequests = + isMessageRequestEnabled && window.lokiFeatureFlags?.useMessageRequests; + return _.filter(sortedConversations, conversation => { + return pushToMessageRequests && !conversation.isApproved && !conversation.isBlocked; + }); +}; + +export const getConversationRequests = createSelector( + getSortedConversations, + getIsMessageRequestsEnabled, _getConversationRequests ); export const getLeftPaneLists = createSelector( - getConversationLookup, - getConversationComparator, + getSortedConversations, getIsMessageRequestsEnabled, - getSelectedConversationKey, _getLeftPaneLists ); diff --git a/ts/test/session/unit/selectors/conversations_test.ts b/ts/test/session/unit/selectors/conversations_test.ts index 30e5bfb89..d3b8c9677 100644 --- a/ts/test/session/unit/selectors/conversations_test.ts +++ b/ts/test/session/unit/selectors/conversations_test.ts @@ -4,11 +4,11 @@ import { ConversationTypeEnum } from '../../../../models/conversation'; import { ConversationLookupType } from '../../../../state/ducks/conversations'; import { _getConversationComparator, - _getLeftPaneLists, + _getSortedConversations, } from '../../../../state/selectors/conversations'; describe('state/selectors/conversations', () => { - describe('#getLeftPaneList', () => { + describe('#getSortedConversationsList', () => { // tslint:disable-next-line: max-func-body-length it('sorts conversations based on timestamp then by intl-friendly title', () => { const i18n = (key: string) => key; @@ -160,7 +160,7 @@ describe('state/selectors/conversations', () => { }, }; const comparator = _getConversationComparator(i18n); - const { conversations } = _getLeftPaneLists(data, comparator); + const conversations = _getSortedConversations(data, comparator); assert.strictEqual(conversations[0].name, 'First!'); assert.strictEqual(conversations[1].name, 'Á'); @@ -169,7 +169,7 @@ describe('state/selectors/conversations', () => { }); }); - describe('#getLeftPaneListWithPinned', () => { + describe('#getSortedConversationsWithPinned', () => { // tslint:disable-next-line: max-func-body-length it('sorts conversations based on pin, timestamp then by intl-friendly title', () => { const i18n = (key: string) => key; @@ -325,7 +325,7 @@ describe('state/selectors/conversations', () => { }, }; const comparator = _getConversationComparator(i18n); - const { conversations } = _getLeftPaneLists(data, comparator); + const conversations = _getSortedConversations(data, comparator); assert.strictEqual(conversations[0].name, 'Á'); assert.strictEqual(conversations[1].name, 'C'); From f17b923addb7ef7c644080adb52bd9276686acc1 Mon Sep 17 00:00:00 2001 From: warrickct Date: Fri, 26 Nov 2021 15:29:57 +1100 Subject: [PATCH 53/70] Fixing rimraf transpile bug. Adding PR fixes - icon buttons. --- package.json | 2 +- stylesheets/_modules.scss | 4 ++++ ts/components/ConversationListItem.tsx | 23 +++++++++++------- .../session/LeftPaneMessageSection.tsx | 24 +++---------------- ts/state/selectors/conversations.ts | 2 +- 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 7a4f72254..98441bd91 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "transpile": "tsc --incremental", "transpile:watch": "tsc -w", "integration-test": "mocha --recursive --exit --timeout 30000 \"./ts/test-integration/**/*.test.js\" \"./ts/test/*.test.js\"", - "clean-transpile": "rimraf 'ts/**/*.js ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;", + "clean-transpile": "rimraf 'ts/**/*.js' 'ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;", "ready": "yarn clean-transpile; yarn grunt && yarn lint-full && yarn test", "build:webpack:sql-worker": "cross-env NODE_ENV=production webpack -c webpack-sql-worker.config.ts", "sedtoAppImage": "sed -i 's/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/\"target\": \"AppImage\"/g' package.json", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index cda5a79b5..badeba855 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -888,6 +888,10 @@ flex-direction: column; align-items: stretch; overflow: hidden; + + .session-icon-button:first-child { + margin-right: $session-margin-sm; + } } .module-conversation-list-item__header { diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 463a90a88..5131c8b49 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -22,13 +22,12 @@ import { } from '../state/ducks/conversations'; import _ from 'underscore'; import { useMembersAvatars } from '../hooks/useMembersAvatar'; -import { SessionIcon } from './session/icon'; +import { SessionIcon, SessionIconButton } from './session/icon'; import { useDispatch, useSelector } from 'react-redux'; import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; import { Flex } from './basic/Flex'; -import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { updateUserDetailsModal } from '../state/ducks/modalDialog'; import { approveConversation, blockConvoById } from '../interactions/conversationInteractions'; @@ -364,17 +363,25 @@ const ConversationListItem = (props: Props) => { flexDirection="row" justifyContent="flex-end" > - - { await approveConversation(conversationId); }} - text={window.i18n('accept')} + backgroundColor="var(--color-accent)" + iconColor="var(--color-foreground-primary)" + iconPadding="var(--margins-xs)" + borderRadius="2px" /> ) : null} diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 6c3746d4b..79d7874b3 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -240,27 +240,9 @@ export class LeftPaneMessageSection extends React.Component { return; } - const conversationRequests = conversations.filter(conversation => { - // Add Open Group to list as soon as the name has been set - if ( - conversation.isPublic() && - (!conversation.get('name') || conversation.get('name') === 'Unknown group') - ) { - return false; - } - - // Remove all invalid conversations and conversatons of devices associated - // with cancelled attempted links - if (!conversation.isPublic && !conversation.get('active_at')) { - return false; - } - - if (conversation.attributes.isApproved || !conversation.get('active_at')) { - return false; - } - - return true; - }); + const conversationRequests = conversations.filter( + c => c.isPrivate() && c.get('active_at') && c.get('isApproved') + ); let syncRequired = false; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 07927f2fd..9b0364900 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -432,7 +432,7 @@ export const _getConversationRequests = ( isMessageRequestEnabled?: boolean ): Array => { const pushToMessageRequests = - isMessageRequestEnabled && window.lokiFeatureFlags?.useMessageRequests; + isMessageRequestEnabled && window?.lokiFeatureFlags?.useMessageRequests; return _.filter(sortedConversations, conversation => { return pushToMessageRequests && !conversation.isApproved && !conversation.isBlocked; }); From cf44896a037be7ca3b12b827373fd7c7de7674d1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 29 Nov 2021 17:40:46 +1100 Subject: [PATCH 54/70] Minor call tweaks (#2051) * show missed-call,started-call and answered call notification in chat * fix types for createLastMessageUpdate * show incoming dialog if we have a pending call when enable call receptio * simplify a bit the avatar component * move disableDrag to a custom hook * speed up hash colors of avatarPlaceHolders * fixup text selection and double click reply on message * keep avatar decoded items longer before releasing memory * add incoming/outgoing/missed call notification also, merge that notification with the timer and group notification component * hangup call if no answer after 30sec * refactor SessionInput using hook + add testid field for recovery * disable message request feature flag for now * fix merge issue * force loading screen to be black instead of white for our dark theme user's eyes safety --- _locales/en/messages.json | 4 +- js/modules/types/conversation.js | 2 - package.json | 1 - preload.js | 2 +- stylesheets/_global.scss | 3 + stylesheets/_modules.scss | 183 --------------- stylesheets/_session.scss | 2 - stylesheets/_theme_dark.scss | 20 -- ts/components/Avatar.tsx | 41 ++-- .../AvatarPlaceHolder/AvatarPlaceHolder.tsx | 73 ++++-- .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 4 +- ts/components/AvatarPlaceHolder/index.ts | 2 - ts/components/ConversationListItem.tsx | 8 +- ts/components/Intl.tsx | 76 ------- ts/components/Lightbox.tsx | 13 +- .../conversation/ConversationHeader.tsx | 21 +- .../DataExtractionNotification.tsx | 2 +- ts/components/conversation/ExpireTimer.tsx | 2 +- .../conversation/GroupNotification.tsx | 44 ++-- ts/components/conversation/H5AudioPlayer.tsx | 6 +- ts/components/conversation/Image.tsx | 13 +- ts/components/conversation/MessageDetail.tsx | 2 +- .../conversation/MissedCallNotification.tsx | 43 ---- ts/components/conversation/Quote.tsx | 15 +- .../conversation/ReadableMessage.tsx | 1 + .../conversation/TimerNotification.tsx | 50 ++--- .../media-gallery/MediaGridItem.tsx | 14 +- .../message/ClickToTrustSender.tsx | 2 +- .../message/MessageContentWithStatus.tsx | 20 +- .../conversation/message/MessagePreview.tsx | 2 +- .../message/OutgoingMessageStatus.tsx | 8 +- .../notification-bubble/CallNotification.tsx | 66 ++++++ .../NotificationBubble.tsx | 52 +++++ ts/components/dialog/SessionModal.tsx | 2 +- .../session/LeftPaneSectionHeader.tsx | 3 +- ts/components/session/SessionButton.tsx | 8 +- .../session/SessionClosableOverlay.tsx | 2 +- ts/components/session/SessionDropdown.tsx | 2 +- ts/components/session/SessionDropdownItem.tsx | 2 +- ts/components/session/SessionInput.tsx | 209 ++++++++---------- .../session/SessionJoinableDefaultRooms.tsx | 1 + .../session/SessionMemberListItem.tsx | 2 +- ts/components/session/SessionWrapperModal.tsx | 2 +- .../calling/DraggableCallContainer.tsx | 2 +- .../calling/InConversationCallContainer.tsx | 2 +- .../session/calling/IncomingCallDialog.tsx | 15 +- .../conversation/SessionMessagesList.tsx | 14 +- .../SessionQuotedMessageComposition.tsx | 2 +- ts/components/session/icon/Icons.tsx | 21 ++ .../registration/RegistrationUserDetails.tsx | 2 + .../session/registration/SignInTab.tsx | 1 + .../settings/section/CategoryPrivacy.tsx | 2 + ts/hooks/useDisableDrag.ts | 13 ++ ts/hooks/useEncryptedFileFetch.ts | 4 +- ts/hooks/useParamSelector.ts | 13 ++ ts/interactions/conversationInteractions.ts | 2 +- ts/models/conversation.ts | 15 +- ts/models/message.ts | 25 ++- ts/models/messageType.ts | 6 +- ts/receiver/callMessage.ts | 6 +- .../crypto/DecryptedAttachmentsManager.ts | 36 ++- ts/session/utils/RingingManager.ts | 4 + ts/session/utils/User.ts | 9 +- ts/session/utils/calling/CallManager.ts | 161 +++++++++++--- ts/state/ducks/conversations.ts | 7 +- ts/state/selectors/conversations.ts | 21 +- ts/test/types/Conversation_test.ts | 11 +- ts/types/Conversation.ts | 15 +- ts/types/Message.ts | 2 +- ts/util/attachmentsUtil.ts | 2 +- 70 files changed, 699 insertions(+), 744 deletions(-) delete mode 100644 ts/components/AvatarPlaceHolder/index.ts delete mode 100644 ts/components/Intl.tsx delete mode 100644 ts/components/conversation/MissedCallNotification.tsx create mode 100644 ts/components/conversation/notification-bubble/CallNotification.tsx create mode 100644 ts/components/conversation/notification-bubble/NotificationBubble.tsx create mode 100644 ts/hooks/useDisableDrag.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a6242ffb4..50b27172b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -460,5 +460,7 @@ "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", - "menuCall": "Call" + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/js/modules/types/conversation.js b/js/modules/types/conversation.js index efca69dfd..82beb2b87 100644 --- a/js/modules/types/conversation.js +++ b/js/modules/types/conversation.js @@ -1,7 +1,6 @@ /* global crypto */ const { isFunction } = require('lodash'); -const { createLastMessageUpdate } = require('../../../ts/types/Conversation'); const { arrayBufferToBase64 } = require('../crypto'); async function computeHash(arraybuffer) { @@ -80,6 +79,5 @@ async function deleteExternalFiles(conversation, options = {}) { module.exports = { deleteExternalFiles, maybeUpdateAvatar, - createLastMessageUpdate, arrayBufferToBase64, }; diff --git a/package.json b/package.json index 98441bd91..5ae1ae014 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "integration-test": "mocha --recursive --exit --timeout 30000 \"./ts/test-integration/**/*.test.js\" \"./ts/test/*.test.js\"", "clean-transpile": "rimraf 'ts/**/*.js' 'ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;", "ready": "yarn clean-transpile; yarn grunt && yarn lint-full && yarn test", - "build:webpack:sql-worker": "cross-env NODE_ENV=production webpack -c webpack-sql-worker.config.ts", "sedtoAppImage": "sed -i 's/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/\"target\": \"AppImage\"/g' package.json", "sedtoDeb": "sed -i 's/\"target\": \"AppImage\"/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/g' package.json" }, diff --git a/preload.js b/preload.js index ebe50c750..cd6fa895e 100644 --- a/preload.js +++ b/preload.js @@ -39,7 +39,7 @@ window.isBehindProxy = () => Boolean(config.proxyUrl); window.lokiFeatureFlags = { useOnionRequests: true, - useMessageRequests: true, + useMessageRequests: false, useCallMessage: true, }; diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 06d10b1a5..8d3db0e71 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -13,6 +13,7 @@ body { margin: 0; font-family: $session-font-default; font-size: 14px; + letter-spacing: 0.3px; } // scrollbars @@ -230,6 +231,8 @@ $loading-height: 16px; display: flex; align-items: center; user-select: none; + // force this to black, to stay consistent with the password prompt being in dark mode too. + background-color: black; .content { margin-inline-start: auto; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index badeba855..95ae355e8 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -276,185 +276,6 @@ font-style: italic; } -.module-message__typing-container { - height: 16px; - padding-bottom: 20px; - display: flex; - flex-direction: row; - align-items: center; -} - -// Module: Contact Detail - -.module-contact-detail { - text-align: center; - max-width: 300px; - margin-inline-start: auto; - margin-inline-end: auto; -} - -.module-contact-detail__avatar { - margin-bottom: 4px; -} - -.module-contact-detail__contact-name { - font-size: 20px; - font-weight: bold; -} - -.module-contact-detail__contact-method { - font-size: 14px; - margin-top: 10px; -} - -.module-contact-detail__send-message { - cursor: pointer; - - border-radius: 4px; - background-color: $blue; - display: inline-block; - padding: 6px; - margin-top: 20px; - - color: $color-white; - - flex-direction: column; - align-items: center; - - button { - @include button-reset; - } -} - -.module-contact-detail__send-message__inner { - display: flex; - align-items: center; -} - -.module-contact-detail__additional-contact { - text-align: left; - border-top: 1px solid $color-light-10; - margin-top: 15px; - padding-top: 8px; -} - -.module-contact-detail__additional-contact__type { - color: $color-light-45; - font-size: 12px; - margin-bottom: 3px; -} - -// Module: Group Notification - -.module-group-notification { - margin-top: 14px; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.3px; - color: $color-gray-60; - text-align: center; -} - -.module-group-notification__change, -.module-timer-notification__message { - background: var(--color-fake-chat-bubble-background); - color: var(--color-text); - - width: 90%; - max-width: 700px; - margin: 10px auto; - padding: 5px 0px; - border-radius: 4px; - word-break: break-word; -} - -.module-group-notification__contact { - font-family: $session-font-default; - font-weight: bold; -} - -// Module: Timer Notification - -.module-timer-notification { - margin-top: 20px; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.3px; - color: $color-gray-60; - text-align: center; -} - -.module-timer-notification__contact { - font-family: $session-font-default; - font-weight: bold; - padding-inline-end: $session-margin-xs; -} - -.module-timer-notification__icon-container { - margin-inline-start: auto; - margin-inline-end: auto; - display: inline-flex; - flex-direction: row; - align-items: center; - margin-bottom: 4px; -} - -.module-timer-notification__icon { - height: 20px; - width: 20px; - display: inline-block; - @include color-svg('../images/timer.svg', $color-gray-60); -} - -.module-timer-notification__icon--disabled { - @include color-svg('../images/timer-disabled.svg', $color-gray-60); -} - -.module-timer-notification__icon-label { - font-size: 11px; - line-height: 16px; - letter-spacing: 0.3px; - margin-inline-start: 6px; - text-transform: uppercase; - - // Didn't seem centered otherwise - margin-top: 1px; -} - -.module-timer-notification__message { - display: grid; - grid-template-columns: 40px auto 40px; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.3px; - - & > div { - align-self: center; - justify-self: center; - align-items: center; - justify-content: center; - } - - .module-contact-name__profile-name { - text-align: center; - } - - .module-message__author__profile-name { - margin-inline-end: $session-margin-xs; - } -} - -.module-notification--with-click-handler { - cursor: pointer; -} - -.module-notification__icon { - height: 24px; - width: 24px; - margin-inline-start: auto; - margin-inline-end: auto; -} - // Module: Contact List Item .module-contact-list-item { @@ -549,10 +370,6 @@ } } -.module-conversation-header__title__profile-name { - font-style: italic; -} - .module-conversation-header__expiration { display: flex; flex-direction: row; diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 20241ba3a..b60aa76a2 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -269,7 +269,6 @@ textarea { .module-conversation-header__title-flex, .module-conversation-header__title { - font-family: $session-font-accent; font-weight: bold; width: 100%; display: flex; @@ -278,7 +277,6 @@ textarea { &-text { @include session-color-subtle(var(--color-text)); - font-family: $session-font-default; font-weight: 300; font-size: $session-font-sm; line-height: $session-font-sm; diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 93ffc3116..2d4c6df91 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -225,26 +225,6 @@ color: $color-light-45; } - // Module: Group Notification - - .module-group-notification { - color: $color-dark-30; - } - - // Module: Timer Notification - - .module-timer-notification { - color: $color-dark-30; - } - - .module-timer-notification__icon { - @include color-svg('../images/timer.svg', $color-dark-30); - } - - .module-timer-notification__icon--disabled { - @include color-svg('../images/timer-disabled.svg', $color-dark-30); - } - // Module: Contact List Item .module-contact-list-item { diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 6b3537155..3a4bfc1ca 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -1,11 +1,15 @@ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; - -import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder'; import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; import _ from 'underscore'; -import { useMembersAvatars } from '../hooks/useMembersAvatars'; -import { useAvatarPath, useConversationUsername } from '../hooks/useParamSelector'; +import { + useAvatarPath, + useConversationUsername, + useIsClosedGroup, +} from '../hooks/useParamSelector'; +import { AvatarPlaceHolder } from './AvatarPlaceHolder/AvatarPlaceHolder'; +import { ClosedGroupAvatar } from './AvatarPlaceHolder/ClosedGroupAvatar'; +import { useDisableDrag } from '../hooks/useDisableDrag'; export enum AvatarSize { XS = 28, @@ -19,7 +23,7 @@ export enum AvatarSize { type Props = { forcedAvatarPath?: string | null; forcedName?: string; - pubkey?: string; + pubkey: string; size: AvatarSize; base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data onAvatarClick?: () => void; @@ -28,17 +32,10 @@ type Props = { const Identicon = (props: Props) => { const { size, forcedName, pubkey } = props; - const userName = forcedName || '0'; + const displayName = useConversationUsername(pubkey); + const userName = forcedName || displayName || '0'; - return ( - - ); + return ; }; const NoImage = ( @@ -66,10 +63,7 @@ const AvatarImage = (props: { }) => { const { avatarPath, base64Data, name, imageBroken, handleImageError } = props; - const onDragStart = useCallback((e: any) => { - e.preventDefault(); - return false; - }, []); + const disableDrag = useDisableDrag(); if ((!avatarPath && !base64Data) || imageBroken) { return null; @@ -79,7 +73,7 @@ const AvatarImage = (props: { return ( {window.i18n('contactAvatarAlt', @@ -90,13 +84,13 @@ const AvatarInner = (props: Props) => { const { base64Data, size, pubkey, forcedAvatarPath, forcedName, dataTestId } = props; const [imageBroken, setImageBroken] = useState(false); - const closedGroupMembers = useMembersAvatars(pubkey); + const isClosedGroupAvatar = useIsClosedGroup(pubkey); const avatarPath = useAvatarPath(pubkey); const name = useConversationUsername(pubkey); // contentType is not important - const { urlToLoad } = useEncryptedFileFetch(forcedAvatarPath || avatarPath || '', ''); + const { urlToLoad } = useEncryptedFileFetch(forcedAvatarPath || avatarPath || '', '', true); const handleImageError = () => { window.log.warn( 'Avatar: Image failed to load; failing over to placeholder', @@ -106,7 +100,6 @@ const AvatarInner = (props: Props) => { setImageBroken(true); }; - const isClosedGroupAvatar = Boolean(closedGroupMembers?.length); const hasImage = (base64Data || urlToLoad) && !imageBroken && !isClosedGroupAvatar; const isClickable = !!props.onAvatarClick; diff --git a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx index fed8d3d41..2aa30a550 100644 --- a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx +++ b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -4,13 +4,10 @@ import { getInitials } from '../../util/getInitials'; type Props = { diameter: number; name: string; - pubkey?: string; - colors: Array; - borderColor: string; + pubkey: string; }; const sha512FromPubkey = async (pubkey: string): Promise => { - // tslint:disable-next-line: await-promise const buf = await crypto.subtle.digest('SHA-512', new TextEncoder().encode(pubkey)); // tslint:disable: prefer-template restrict-plus-operands @@ -19,34 +16,69 @@ const sha512FromPubkey = async (pubkey: string): Promise => { .join(''); }; -export const AvatarPlaceHolder = (props: Props) => { - const { borderColor, colors, pubkey, diameter, name } = props; - const [sha512Seed, setSha512Seed] = useState(undefined as string | undefined); +// do not do this on every avatar, just cache the values so we can reuse them accross the app +// key is the pubkey, value is the hash +const cachedHashes = new Map(); + +const avatarPlaceholderColors = ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a']; +const avatarBorderColor = '#00000059'; + +function useHashBasedOnPubkey(pubkey: string) { + const [hash, setHash] = useState(undefined); + const [loading, setIsLoading] = useState(true); + useEffect(() => { - let isSubscribed = true; + const cachedHash = cachedHashes.get(pubkey); + + if (cachedHash) { + setHash(cachedHash); + setIsLoading(false); + return; + } + setIsLoading(true); + let isInProgress = true; if (!pubkey) { - if (isSubscribed) { - setSha512Seed(undefined); + if (isInProgress) { + setIsLoading(false); + + setHash(undefined); } return; } void sha512FromPubkey(pubkey).then(sha => { - if (isSubscribed) { - setSha512Seed(sha); + if (isInProgress) { + setIsLoading(false); + // Generate the seed simulate the .hashCode as Java + if (sha) { + const hashed = parseInt(sha.substring(0, 12), 16) || 0; + setHash(hashed); + cachedHashes.set(pubkey, hashed); + + return; + } + setHash(undefined); } }); return () => { - isSubscribed = false; + isInProgress = false; }; - }, [pubkey, name]); + }, [pubkey]); + + return { loading, hash }; +} + +export const AvatarPlaceHolder = (props: Props) => { + const { pubkey, diameter, name } = props; + + const { hash, loading } = useHashBasedOnPubkey(pubkey); const diameterWithoutBorder = diameter - 2; const viewBox = `0 0 ${diameter} ${diameter}`; const r = diameter / 2; const rWithoutBorder = diameterWithoutBorder / 2; - if (!sha512Seed) { + if (loading || !hash) { // return grey circle return ( @@ -57,7 +89,7 @@ export const AvatarPlaceHolder = (props: Props) => { r={rWithoutBorder} fill="#d2d2d3" shapeRendering="geometricPrecision" - stroke={borderColor} + stroke={avatarBorderColor} strokeWidth="1" /> @@ -68,12 +100,9 @@ export const AvatarPlaceHolder = (props: Props) => { const initial = getInitials(name)?.toLocaleUpperCase() || '0'; const fontSize = diameter * 0.5; - // Generate the seed simulate the .hashCode as Java - const hash = parseInt(sha512Seed.substring(0, 12), 16) || 0; - - const bgColorIndex = hash % colors.length; + const bgColorIndex = hash % avatarPlaceholderColors.length; - const bgColor = colors[bgColorIndex]; + const bgColor = avatarPlaceholderColors[bgColorIndex]; return ( @@ -84,7 +113,7 @@ export const AvatarPlaceHolder = (props: Props) => { r={rWithoutBorder} fill={bgColor} shapeRendering="geometricPrecision" - stroke={borderColor} + stroke={avatarBorderColor} strokeWidth="1" /> { return (
- - + +
); }; diff --git a/ts/components/AvatarPlaceHolder/index.ts b/ts/components/AvatarPlaceHolder/index.ts deleted file mode 100644 index f6f819bd6..000000000 --- a/ts/components/AvatarPlaceHolder/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AvatarPlaceHolder } from './AvatarPlaceHolder'; -export { ClosedGroupAvatar } from './ClosedGroupAvatar'; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index d6e308cb1..f1c56e0a1 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -20,7 +20,7 @@ import { ReduxConversationType, } from '../state/ducks/conversations'; import _ from 'underscore'; -import { SessionIcon } from './session/icon'; +import { SessionIcon, SessionIconButton } from './session/icon'; import { useDispatch, useSelector } from 'react-redux'; import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; @@ -83,7 +83,7 @@ const HeaderItem = (props: { const pinIcon = isMessagesSection && isPinned ? ( - + ) : null; const NotificationSettingIcon = () => { @@ -96,11 +96,11 @@ const HeaderItem = (props: { return null; case 'disabled': return ( - + ); case 'mentions_only': return ( - + ); default: return null; diff --git a/ts/components/Intl.tsx b/ts/components/Intl.tsx deleted file mode 100644 index f8ddfb5df..000000000 --- a/ts/components/Intl.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; - -import { RenderTextCallbackType } from '../types/Util'; - -type FullJSX = Array | JSX.Element | string; - -interface Props { - /** The translation string id */ - id: string; - components?: Array; - renderText?: RenderTextCallbackType; -} - -export class Intl extends React.Component { - public static defaultProps: Partial = { - renderText: ({ text }) => text, - }; - - public getComponent(index: number): FullJSX | undefined { - const { id, components } = this.props; - - if (!components || !components.length || components.length <= index) { - // tslint:disable-next-line no-console - console.log(`Error: Intl missing provided components for id ${id}, index ${index}`); - - return; - } - - return components[index]; - } - - public render() { - const { id, renderText } = this.props; - - const text = window.i18n(id); - const results: Array = []; - const FIND_REPLACEMENTS = /\$[^$]+\$/g; - - // We have to do this, because renderText is not required in our Props object, - // but it is always provided via defaultProps. - if (!renderText) { - return; - } - - let componentIndex = 0; - let key = 0; - let lastTextIndex = 0; - let match = FIND_REPLACEMENTS.exec(text); - - if (!match) { - return renderText({ text, key: 0 }); - } - - while (match) { - if (lastTextIndex < match.index) { - const textWithNoReplacements = text.slice(lastTextIndex, match.index); - results.push(renderText({ text: textWithNoReplacements, key: key })); - key += 1; - } - - results.push(this.getComponent(componentIndex)); - componentIndex += 1; - - // @ts-ignore - lastTextIndex = FIND_REPLACEMENTS.lastIndex; - match = FIND_REPLACEMENTS.exec(text); - } - - if (lastTextIndex < text.length) { - results.push(renderText({ text: text.slice(lastTextIndex), key: key })); - key += 1; - } - - return results; - } -} diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index e777d43ff..6096a2381 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -1,6 +1,6 @@ // tslint:disable:react-a11y-anchors -import React, { useCallback, useRef } from 'react'; +import React, { useRef } from 'react'; import is from '@sindresorhus/is'; @@ -14,6 +14,7 @@ import useUnmount from 'react-use/lib/useUnmount'; import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; import { useDispatch } from 'react-redux'; import { showLightBox } from '../state/ducks/conversations'; +import { useDisableDrag } from '../hooks/useDisableDrag'; const Colors = { TEXT_SECONDARY: '#bbb', @@ -204,15 +205,10 @@ export const LightboxObject = ({ renderedRef: React.MutableRefObject; onObjectClick: (event: any) => any; }) => { - const { urlToLoad } = useEncryptedFileFetch(objectURL, contentType); + const { urlToLoad } = useEncryptedFileFetch(objectURL, contentType, false); const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); - const onDragStart = useCallback((e: any) => { - e.preventDefault(); - return false; - }, []); - // auto play video on showing a video attachment useUnmount(() => { if (!renderedRef?.current) { @@ -220,12 +216,13 @@ export const LightboxObject = ({ } renderedRef.current.pause.pause(); }); + const disableDrag = useDisableDrag(); if (isImageTypeSupported) { return ( {window.i18n('lightboxImageAlt')} void; showBackButton: boolean }) => } return ( - + ); }; @@ -249,22 +250,14 @@ const ConversationHeaderTitle = () => { const headerTitleProps = useSelector(getConversationHeaderTitleProps); const notificationSetting = useSelector(getCurrentNotificationSettingText); const isRightPanelOn = useSelector(isRightPanelShowing); + + const convoName = useConversationUsername(headerTitleProps?.conversationKey); const dispatch = useDispatch(); if (!headerTitleProps) { return null; } - const { - conversationKey, - profileName, - isGroup, - isPublic, - members, - subscriberCount, - isMe, - isKickedFromGroup, - name, - } = headerTitleProps; + const { isGroup, isPublic, members, subscriberCount, isMe, isKickedFromGroup } = headerTitleProps; const { i18n } = window; @@ -294,8 +287,6 @@ const ConversationHeaderTitle = () => { ? `${memberCountText} ● ${notificationSubtitle}` : `${notificationSubtitle}`; - const title = profileName || name || conversationKey; - return (
{ }} role="button" > - {title} + {convoName} diff --git a/ts/components/conversation/DataExtractionNotification.tsx b/ts/components/conversation/DataExtractionNotification.tsx index 1ac60d806..c19ddba9e 100644 --- a/ts/components/conversation/DataExtractionNotification.tsx +++ b/ts/components/conversation/DataExtractionNotification.tsx @@ -31,7 +31,7 @@ export const DataExtractionNotification = (props: PropsForDataExtractionNotifica margin={'var(--margins-sm)'} id={`msg-${messageId}`} > - + diff --git a/ts/components/conversation/ExpireTimer.tsx b/ts/components/conversation/ExpireTimer.tsx index 08e02a523..d49ebb33a 100644 --- a/ts/components/conversation/ExpireTimer.tsx +++ b/ts/components/conversation/ExpireTimer.tsx @@ -64,7 +64,7 @@ export const ExpireTimer = (props: Props) => { return ( - + ); }; diff --git a/ts/components/conversation/GroupNotification.tsx b/ts/components/conversation/GroupNotification.tsx index b74263483..d82e37f3a 100644 --- a/ts/components/conversation/GroupNotification.tsx +++ b/ts/components/conversation/GroupNotification.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { flatten } from 'lodash'; -import { Intl } from '../Intl'; import { PropsForGroupUpdate, PropsForGroupUpdateAdd, @@ -11,6 +9,7 @@ import { } from '../../state/ducks/conversations'; import _ from 'underscore'; import { ReadableMessage } from './ReadableMessage'; +import { NotificationBubble } from './notification-bubble/NotificationBubble'; // This component is used to display group updates in the conversation view. // This is a not a "notification" as the name suggests, but a message inside the conversation @@ -25,34 +24,22 @@ function isTypeWithContact(change: PropsForGroupUpdateType): change is TypeWithC } function getPeople(change: TypeWithContacts) { - return _.compact( - flatten( - (change.contacts || []).map((contact, index) => { - const element = ( - - {contact.profileName || contact.pubkey} - - ); - - return [index > 0 ? ', ' : null, element]; - }) - ) - ); + return change.contacts?.map(c => c.profileName || c.pubkey).join(', '); } -function renderChange(change: PropsForGroupUpdateType) { +const ChangeItem = (change: PropsForGroupUpdateType): string => { const people = isTypeWithContact(change) ? getPeople(change) : []; + switch (change.type) { case 'name': - return `${window.i18n('titleIsNow', [change.newName || ''])}`; + return window.i18n('titleIsNow', change.newName || ''); case 'add': if (!change.contacts || !change.contacts.length) { throw new Error('Group update add is missing contacts'); } const joinKey = change.contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup'; - - return ; + return window.i18n(joinKey, people); case 'remove': if (change.isMe) { return window.i18n('youLeftTheGroup'); @@ -63,8 +50,8 @@ function renderChange(change: PropsForGroupUpdateType) { } const leftKey = change.contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup'; + return window.i18n(leftKey, people); - return ; case 'kicked': if (change.isMe) { return window.i18n('youGotKickedFromGroup'); @@ -76,17 +63,20 @@ function renderChange(change: PropsForGroupUpdateType) { const kickedKey = change.contacts.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup'; + return window.i18n(kickedKey, people); - return ; case 'general': return window.i18n('updatedTheGroup'); default: - window.log.error('Missing case error'); + throw new Error('Missing case error'); } -} +}; export const GroupNotification = (props: PropsForGroupUpdate) => { const { changes, messageId, receivedAt, isUnread } = props; + + const textChange = changes.map(ChangeItem)[0]; + return ( { isUnread={isUnread} key={`readable-message-${messageId}`} > -
- {(changes || []).map((change, index) => ( -
- {renderChange(change)} -
- ))} -
+
); }; diff --git a/ts/components/conversation/H5AudioPlayer.tsx b/ts/components/conversation/H5AudioPlayer.tsx index 4f2c23248..f8a3ecd8f 100644 --- a/ts/components/conversation/H5AudioPlayer.tsx +++ b/ts/components/conversation/H5AudioPlayer.tsx @@ -19,7 +19,7 @@ export const AudioPlayerWithEncryptedFile = (props: { }) => { const dispatch = useDispatch(); const [playbackSpeed, setPlaybackSpeed] = useState(1.0); - const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType); + const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType, false); const player = useRef(null); const autoPlaySetting = useSelector(getAudioAutoplay); @@ -104,10 +104,10 @@ export const AudioPlayerWithEncryptedFile = (props: { ]} customIcons={{ play: ( - + ), pause: ( - + ), }} /> diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index d052213ca..73b8f2866 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -4,6 +4,7 @@ import classNames from 'classnames'; import { Spinner } from '../basic/Spinner'; import { AttachmentType, AttachmentTypeWithPath } from '../../types/Attachment'; import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; +import { useDisableDrag } from '../../hooks/useDisableDrag'; type Props = { alt: string; @@ -48,17 +49,13 @@ export const Image = (props: Props) => { width, } = props; - const onDragStart = useCallback((e: any) => { - e.preventDefault(); - return false; - }, []); - const onErrorUrlFilterering = useCallback(() => { if (url && onError) { onError(); } return; }, [url, onError]); + const disableDrag = useDisableDrag(); const { caption } = attachment || { caption: null }; let { pending } = attachment || { pending: true }; @@ -68,7 +65,7 @@ export const Image = (props: Props) => { } const canClick = onClick && !pending; const role = canClick ? 'button' : undefined; - const { loading, urlToLoad } = useEncryptedFileFetch(url || '', attachment.contentType); + const { loading, urlToLoad } = useEncryptedFileFetch(url || '', attachment.contentType, false); // data will be url if loading is finished and '' if not const srcData = !loading ? urlToLoad : ''; @@ -118,7 +115,7 @@ export const Image = (props: Props) => { height: forceSquare ? `${height}px` : '', }} src={srcData} - onDragStart={onDragStart} + onDragStart={disableDrag} /> )} {caption ? ( @@ -126,7 +123,7 @@ export const Image = (props: Props) => { className="module-image__caption-icon" src="images/caption-shadow.svg" alt={window.i18n('imageCaptionIconAlt')} - onDragStart={onDragStart} + onDragStart={disableDrag} /> ) : null}
{ +const AvatarItem = (props: { pubkey: string }) => { const { pubkey } = props; return ; diff --git a/ts/components/conversation/MissedCallNotification.tsx b/ts/components/conversation/MissedCallNotification.tsx deleted file mode 100644 index ce7ad87f7..000000000 --- a/ts/components/conversation/MissedCallNotification.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import styled from 'styled-components'; -import { PubKey } from '../../session/types'; - -import { PropsForMissedCallNotification } from '../../state/ducks/conversations'; -import { getSelectedConversation } from '../../state/selectors/conversations'; -import { ReadableMessage } from './ReadableMessage'; - -export const StyledFakeMessageBubble = styled.div` - background: var(--color-fake-chat-bubble-background); - color: var(--color-text); - - width: 90%; - max-width: 700px; - margin: 10px auto; - padding: 5px 0px; - border-radius: 4px; - word-break: break-word; - text-align: center; -`; - -export const MissedCallNotification = (props: PropsForMissedCallNotification) => { - const { messageId, receivedAt, isUnread } = props; - - const selectedConvoProps = useSelector(getSelectedConversation); - - const displayName = - selectedConvoProps?.profileName || - selectedConvoProps?.name || - (selectedConvoProps?.id && PubKey.shorten(selectedConvoProps?.id)); - - return ( - - {window.i18n('callMissed', displayName)} - - ); -}; diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index c2c456241..b65a979cf 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -1,6 +1,4 @@ -// tslint:disable:react-this-binding-issue - -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import * as MIME from '../../../ts/types/MIME'; @@ -18,6 +16,7 @@ import { isPublicGroupConversation, } from '../../state/selectors/conversations'; import { noop } from 'underscore'; +import { useDisableDrag } from '../../hooks/useDisableDrag'; export type QuotePropsWithoutListener = { attachment?: QuotedAttachmentType; @@ -116,15 +115,11 @@ export const QuoteImage = (props: { icon?: string; }) => { const { url, icon, contentType, handleImageErrorBound } = props; + const disableDrag = useDisableDrag(); - const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType); + const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType, false); const srcData = !loading ? urlToLoad : ''; - const onDragStart = useCallback((e: any) => { - e.preventDefault(); - return false; - }, []); - const iconElement = icon ? (
@@ -143,7 +138,7 @@ export const QuoteImage = (props: { {window.i18n('quoteThumbnailAlt')} {iconElement} diff --git a/ts/components/conversation/ReadableMessage.tsx b/ts/components/conversation/ReadableMessage.tsx index c4f580279..8ee296ee3 100644 --- a/ts/components/conversation/ReadableMessage.tsx +++ b/ts/components/conversation/ReadableMessage.tsx @@ -131,6 +131,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => { onChange={haveDoneFirstScroll && isAppFocused ? onVisible : noop} triggerOnce={false} trackVisibility={true} + key={`inview-msg-${messageId}`} > {props.children} diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index 0f06a86de..f20b54c81 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -1,41 +1,39 @@ import React from 'react'; -import { Intl } from '../Intl'; - import { missingCaseError } from '../../util/missingCaseError'; -import { SessionIcon } from '../session/icon'; import { PropsForExpirationTimer } from '../../state/ducks/conversations'; import { ReadableMessage } from './ReadableMessage'; +import { NotificationBubble } from './notification-bubble/NotificationBubble'; -const TimerNotificationContent = (props: PropsForExpirationTimer) => { - const { pubkey, profileName, timespan, type, disabled } = props; - const changeKey = disabled ? 'disabledDisappearingMessages' : 'theyChangedTheTimer'; +export const TimerNotification = (props: PropsForExpirationTimer) => { + const { messageId, receivedAt, isUnread, pubkey, profileName, timespan, type, disabled } = props; - const contact = ( - - {profileName || pubkey} - - ); + const contact = profileName || pubkey; + let textToRender: string | undefined; switch (type) { case 'fromOther': - return ; + textToRender = disabled + ? window.i18n('disabledDisappearingMessages', [contact, timespan]) + : window.i18n('theyChangedTheTimer', [contact, timespan]); + break; case 'fromMe': - return disabled + textToRender = disabled ? window.i18n('youDisabledDisappearingMessages') : window.i18n('youChangedTheTimer', [timespan]); + break; case 'fromSync': - return disabled + textToRender = disabled ? window.i18n('disappearingMessagesDisabled') : window.i18n('timerSetOnSync', [timespan]); + break; default: throw missingCaseError(type); } -}; - -export const TimerNotification = (props: PropsForExpirationTimer) => { - const { messageId, receivedAt, isUnread } = props; + if (!textToRender || textToRender.length === 0) { + throw new Error('textToRender invalid key used TimerNotification'); + } return ( { isUnread={isUnread} key={`readable-message-${messageId}`} > -
-
-
- -
- -
- -
-
-
+
); }; diff --git a/ts/components/conversation/media-gallery/MediaGridItem.tsx b/ts/components/conversation/media-gallery/MediaGridItem.tsx index 9383a3467..fb444f1d3 100644 --- a/ts/components/conversation/media-gallery/MediaGridItem.tsx +++ b/ts/components/conversation/media-gallery/MediaGridItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import { isImageTypeSupported, isVideoTypeSupported } from '../../../util/GoogleChrome'; @@ -6,6 +6,7 @@ import { MediaItemType } from '../../LightboxGallery'; import { useEncryptedFileFetch } from '../../../hooks/useEncryptedFileFetch'; import { showLightBox } from '../../../state/ducks/conversations'; import { LightBoxOptions } from '../../session/conversation/SessionConversation'; +import { useDisableDrag } from '../../../hooks/useDisableDrag'; type Props = { mediaItem: MediaItemType; @@ -20,14 +21,11 @@ const MediaGridItemContent = (props: Props) => { const urlToDecrypt = mediaItem.thumbnailObjectUrl || ''; const [imageBroken, setImageBroken] = useState(false); - const { loading, urlToLoad } = useEncryptedFileFetch(urlToDecrypt, contentType); + const { loading, urlToLoad } = useEncryptedFileFetch(urlToDecrypt, contentType, false); - const onDragStart = useCallback((e: any) => { - e.preventDefault(); - return false; - }, []); // data will be url if loading is finished and '' if not const srcData = !loading ? urlToLoad : ''; + const disableDrag = useDisableDrag(); const onImageError = () => { // tslint:disable-next-line no-console @@ -57,7 +55,7 @@ const MediaGridItemContent = (props: Props) => { className="module-media-grid-item__image" src={srcData} onError={onImageError} - onDragStart={onDragStart} + onDragStart={disableDrag} /> ); } else if (contentType && isVideoTypeSupported(contentType)) { @@ -79,7 +77,7 @@ const MediaGridItemContent = (props: Props) => { className="module-media-grid-item__image" src={srcData} onError={onImageError} - onDragStart={onDragStart} + onDragStart={disableDrag} />
diff --git a/ts/components/conversation/message/ClickToTrustSender.tsx b/ts/components/conversation/message/ClickToTrustSender.tsx index f7ecd86f9..831e8c56f 100644 --- a/ts/components/conversation/message/ClickToTrustSender.tsx +++ b/ts/components/conversation/message/ClickToTrustSender.tsx @@ -110,7 +110,7 @@ export const ClickToTrustSender = (props: { messageId: string }) => { return ( - + {window.i18n('clickToTrustContact')} ); diff --git a/ts/components/conversation/message/MessageContentWithStatus.tsx b/ts/components/conversation/message/MessageContentWithStatus.tsx index 217881cc6..4b3751920 100644 --- a/ts/components/conversation/message/MessageContentWithStatus.tsx +++ b/ts/components/conversation/message/MessageContentWithStatus.tsx @@ -45,8 +45,22 @@ export const MessageContentWithStatuses = (props: Props) => { [window.contextMenuShown, props?.messageId, multiSelectMode, props?.isDetailView] ); - const onDoubleClickReplyToMessage = () => { - void replyToMessage(messageId); + const onDoubleClickReplyToMessage = (e: React.MouseEvent) => { + const currentSelection = window.getSelection(); + const currentSelectionString = currentSelection?.toString() || undefined; + + // if multiple word are selected, consider that this double click was actually NOT used to reply to + // but to select + if ( + !currentSelectionString || + currentSelectionString.length === 0 || + !currentSelectionString.includes(' ') + ) { + void replyToMessage(messageId); + currentSelection?.empty(); + e.preventDefault(); + return; + } }; const { messageId, onQuoteClick, ctxMenuID, isDetailView } = props; @@ -61,7 +75,7 @@ export const MessageContentWithStatuses = (props: Props) => { className={classNames('module-message', `module-message--${direction}`)} role="button" onClick={onClickOnMessageOuterContainer} - onDoubleClick={onDoubleClickReplyToMessage} + onDoubleClickCapture={onDoubleClickReplyToMessage} style={{ width: hasAttachments && isTrustedForAttachmentDownload ? 'min-content' : 'auto' }} > diff --git a/ts/components/conversation/message/MessagePreview.tsx b/ts/components/conversation/message/MessagePreview.tsx index e4d49110a..39dadc596 100644 --- a/ts/components/conversation/message/MessagePreview.tsx +++ b/ts/components/conversation/message/MessagePreview.tsx @@ -63,7 +63,7 @@ export const MessagePreview = (props: Props) => {
- +
diff --git a/ts/components/conversation/message/OutgoingMessageStatus.tsx b/ts/components/conversation/message/OutgoingMessageStatus.tsx index c710adfde..9508b708c 100644 --- a/ts/components/conversation/message/OutgoingMessageStatus.tsx +++ b/ts/components/conversation/message/OutgoingMessageStatus.tsx @@ -16,7 +16,7 @@ const MessageStatusSending = () => { const iconColor = 'var(--color-text)'; return ( - + ); }; @@ -26,7 +26,7 @@ const MessageStatusSent = () => { return ( - + ); }; @@ -36,7 +36,7 @@ const MessageStatusRead = () => { return ( - + ); }; @@ -48,7 +48,7 @@ const MessageStatusError = () => { return ( - + ); }; diff --git a/ts/components/conversation/notification-bubble/CallNotification.tsx b/ts/components/conversation/notification-bubble/CallNotification.tsx new file mode 100644 index 000000000..9302a157a --- /dev/null +++ b/ts/components/conversation/notification-bubble/CallNotification.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { PubKey } from '../../../session/types'; + +import { CallNotificationType, PropsForCallNotification } from '../../../state/ducks/conversations'; +import { getSelectedConversation } from '../../../state/selectors/conversations'; +import { SessionIconType } from '../../session/icon'; +import { ReadableMessage } from '../ReadableMessage'; +import { NotificationBubble } from './NotificationBubble'; + +type StyleType = Record< + CallNotificationType, + { notificationTextKey: string; iconType: SessionIconType; iconColor: string } +>; + +const style: StyleType = { + 'missed-call': { + notificationTextKey: 'callMissed', + iconType: 'callMissed', + iconColor: 'var(--color-destructive)', + }, + 'started-call': { + notificationTextKey: 'startedACall', + iconType: 'callOutgoing', + iconColor: 'inherit', + }, + 'answered-a-call': { + notificationTextKey: 'answeredACall', + iconType: 'callIncoming', + iconColor: 'inherit', + }, +}; + +export const CallNotification = (props: PropsForCallNotification) => { + const { messageId, receivedAt, isUnread, notificationType } = props; + + const selectedConvoProps = useSelector(getSelectedConversation); + + const displayName = + selectedConvoProps?.profileName || + selectedConvoProps?.name || + (selectedConvoProps?.id && PubKey.shorten(selectedConvoProps?.id)); + + const styleItem = style[notificationType]; + const notificationText = window.i18n(styleItem.notificationTextKey, displayName); + if (!window.i18n(styleItem.notificationTextKey)) { + throw new Error(`invalid i18n key ${styleItem.notificationTextKey}`); + } + const iconType = styleItem.iconType; + const iconColor = styleItem.iconColor; + + return ( + + + + ); +}; diff --git a/ts/components/conversation/notification-bubble/NotificationBubble.tsx b/ts/components/conversation/notification-bubble/NotificationBubble.tsx new file mode 100644 index 000000000..39d00a697 --- /dev/null +++ b/ts/components/conversation/notification-bubble/NotificationBubble.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import styled from 'styled-components'; +import { SessionIcon, SessionIconType } from '../../session/icon'; + +const NotificationBubbleFlex = styled.div` + display: flex; + background: var(--color-fake-chat-bubble-background); + color: var(--color-text); + width: 90%; + max-width: 700px; + margin: 10px auto; + padding: 5px 10px; + border-radius: 16px; + word-break: break-word; + text-align: center; + align-items: center; +`; + +const NotificationBubbleText = styled.div` + color: inherit; + margin: auto auto; +`; + +const NotificationBubbleIconContainer = styled.div` + margin: auto 10px; + width: 15px; + height: 25px; +`; + +export const NotificationBubble = (props: { + notificationText: string; + iconType?: SessionIconType; + iconColor?: string; +}) => { + const { notificationText, iconType, iconColor } = props; + return ( + + {iconType && ( + + + + )} + {notificationText} + {iconType && } + + ); +}; diff --git a/ts/components/dialog/SessionModal.tsx b/ts/components/dialog/SessionModal.tsx index 3e94fc625..22746bb83 100644 --- a/ts/components/dialog/SessionModal.tsx +++ b/ts/components/dialog/SessionModal.tsx @@ -79,7 +79,7 @@ export class SessionModal extends React.PureComponent {
{showExitIcon ? ( - + ) : null}
{title}
diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx index acadc1fce..3e734e827 100644 --- a/ts/components/session/LeftPaneSectionHeader.tsx +++ b/ts/components/session/LeftPaneSectionHeader.tsx @@ -56,7 +56,7 @@ export const LeftPaneSectionHeader = (props: Props) => { {label && } {buttonIcon && ( - + )}
@@ -80,6 +80,7 @@ const BannerInner = () => { buttonType={SessionButtonType.Default} text={window.i18n('recoveryPhraseRevealButtonText')} onClick={showRecoveryPhraseModal} + dataTestId="reveal-recovery-phrase" /> ); diff --git a/ts/components/session/SessionButton.tsx b/ts/components/session/SessionButton.tsx index 313aaa5f0..38aea1905 100644 --- a/ts/components/session/SessionButton.tsx +++ b/ts/components/session/SessionButton.tsx @@ -28,15 +28,16 @@ type Props = { buttonColor: SessionButtonColor; onClick: any; children?: ReactNode; + dataTestId?: string; }; export const SessionButton = (props: Props) => { - const { buttonType, buttonColor, text, disabled } = props; + const { buttonType, dataTestId, buttonColor, text, disabled, onClick } = props; const clickHandler = (e: any) => { - if (props.onClick) { + if (onClick) { e.stopPropagation(); - props.onClick(); + onClick(); } }; @@ -53,6 +54,7 @@ export const SessionButton = (props: Props) => { className={classNames('session-button', ...buttonTypes, buttonColor, disabled && 'disabled')} role="button" onClick={onClickFn} + data-testid={dataTestId} > {props.children || text}
diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 967dc1b77..eddc9450f 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -156,7 +156,7 @@ export class SessionClosableOverlay extends React.Component { return (
- +
diff --git a/ts/components/session/SessionDropdown.tsx b/ts/components/session/SessionDropdown.tsx index f151bc3ee..4c678251f 100644 --- a/ts/components/session/SessionDropdown.tsx +++ b/ts/components/session/SessionDropdown.tsx @@ -34,7 +34,7 @@ export const SessionDropdown = (props: Props) => { role="button" > {label} - +
{expanded && ( diff --git a/ts/components/session/SessionDropdownItem.tsx b/ts/components/session/SessionDropdownItem.tsx index 94d722c25..7fccb5b98 100644 --- a/ts/components/session/SessionDropdownItem.tsx +++ b/ts/components/session/SessionDropdownItem.tsx @@ -36,7 +36,7 @@ export const SessionDropdownItem = (props: Props) => { role="button" onClick={clickHandler} > - {icon ? : ''} + {icon ? : ''}
{content}
); diff --git a/ts/components/session/SessionInput.tsx b/ts/components/session/SessionInput.tsx index 611ae6450..f02580cdc 100644 --- a/ts/components/session/SessionInput.tsx +++ b/ts/components/session/SessionInput.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import { SessionIconButton } from './icon'; -interface Props { +type Props = { label?: string; error?: string; type?: string; @@ -15,117 +15,100 @@ interface Props { onEnterPressed?: any; autoFocus?: boolean; ref?: any; -} - -interface State { - inputValue: string; - forceShow: boolean; -} - -export class SessionInput extends React.PureComponent { - constructor(props: any) { - super(props); - - this.updateInputValue = this.updateInputValue.bind(this); - this.renderShowHideButton = this.renderShowHideButton.bind(this); - - this.state = { - inputValue: '', - forceShow: false, - }; - } - - public render() { - const { autoFocus, placeholder, type, value, maxLength, enableShowHide, error } = this.props; - const { forceShow } = this.state; - - const correctType = forceShow ? 'text' : type; - - return ( -
- {error ? this.renderError() : this.renderLabel()} - { - this.updateInputValue(e); - }} - className={classNames(enableShowHide ? 'session-input-floating-label-show-hide' : '')} - // just incase onChange isn't triggered - onBlur={e => { - this.updateInputValue(e); - }} - onKeyPress={event => { - if (event.key === 'Enter' && this.props.onEnterPressed) { - this.props.onEnterPressed(); - } - }} - /> - - {enableShowHide && this.renderShowHideButton()} - -
-
- ); - } - - private renderLabel() { - const { inputValue } = this.state; - const { label } = this.props; - - return ( - - ); - } - - private renderError() { - const { error } = this.props; - - return ( - - ); - } - - private renderShowHideButton() { - return ( - { - this.setState({ - forceShow: !this.state.forceShow, - }); + inputDataTestId?: string; +}; + +const LabelItem = (props: { inputValue: string; label?: string }) => { + return ( + + ); +}; + +const ErrorItem = (props: { error: string | undefined }) => { + return ( + + ); +}; + +const ShowHideButton = (props: { toggleForceShow: () => void }) => { + return ; +}; + +export const SessionInput = (props: Props) => { + const { + autoFocus, + placeholder, + type, + value, + maxLength, + enableShowHide, + error, + label, + onValueChanged, + inputDataTestId, + } = props; + const [inputValue, setInputValue] = useState(''); + const [forceShow, setForceShow] = useState(false); + + const correctType = forceShow ? 'text' : type; + + const updateInputValue = (e: React.ChangeEvent) => { + e.preventDefault(); + const val = e.target.value; + setInputValue(val); + if (onValueChanged) { + onValueChanged(val); + } + }; + + return ( +
+ {error ? ( + + ) : ( + + )} + { + if (event.key === 'Enter' && props.onEnterPressed) { + props.onEnterPressed(); + } }} /> - ); - } - - private updateInputValue(e: any) { - e.preventDefault(); - this.setState({ - inputValue: e.target.value, - }); - if (this.props.onValueChanged) { - this.props.onValueChanged(e.target.value); - } - } -} + {enableShowHide && ( + { + setForceShow(!forceShow); + }} + /> + )} +
+
+ ); +}; diff --git a/ts/components/session/SessionJoinableDefaultRooms.tsx b/ts/components/session/SessionJoinableDefaultRooms.tsx index e9abccea7..0925c0b71 100644 --- a/ts/components/session/SessionJoinableDefaultRooms.tsx +++ b/ts/components/session/SessionJoinableDefaultRooms.tsx @@ -74,6 +74,7 @@ const SessionJoinableRoomAvatar = (props: JoinableRoomProps) => { size={AvatarSize.XS} base64Data={props.base64Data} {...props} + pubkey="" onAvatarClick={() => props.onClick(props.completeUrl)} /> ); diff --git a/ts/components/session/SessionMemberListItem.tsx b/ts/components/session/SessionMemberListItem.tsx index f90a03f19..83efcd136 100644 --- a/ts/components/session/SessionMemberListItem.tsx +++ b/ts/components/session/SessionMemberListItem.tsx @@ -27,7 +27,7 @@ type Props = { onUnselect?: (selectedMember: ContactType) => void; }; -const AvatarItem = (props: { memberPubkey?: string }) => { +const AvatarItem = (props: { memberPubkey: string }) => { return ; }; diff --git a/ts/components/session/SessionWrapperModal.tsx b/ts/components/session/SessionWrapperModal.tsx index b03c71bb0..ff615e5c6 100644 --- a/ts/components/session/SessionWrapperModal.tsx +++ b/ts/components/session/SessionWrapperModal.tsx @@ -81,7 +81,7 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
{showExitIcon ? ( - + ) : null}
{title}
diff --git a/ts/components/session/calling/DraggableCallContainer.tsx b/ts/components/session/calling/DraggableCallContainer.tsx index 04b0e9194..194e662eb 100644 --- a/ts/components/session/calling/DraggableCallContainer.tsx +++ b/ts/components/session/calling/DraggableCallContainer.tsx @@ -135,7 +135,7 @@ export const DraggableCallContainer = () => { autoPlay={true} isVideoMuted={remoteStreamVideoIsMuted} /> - {remoteStreamVideoIsMuted && ( + {remoteStreamVideoIsMuted && ongoingCallPubkey && ( diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index 87fa01175..e0b5f1d67 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -158,7 +158,7 @@ export const InConversationCallContainer = () => { videoRefRemote.current.muted = true; } - if (!ongoingCallWithFocused) { + if (!ongoingCallWithFocused || !ongoingCallPubkey) { return null; } diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx index 3e6dd5500..d812044a1 100644 --- a/ts/components/session/calling/IncomingCallDialog.tsx +++ b/ts/components/session/calling/IncomingCallDialog.tsx @@ -6,6 +6,7 @@ import _ from 'underscore'; import { useConversationUsername } from '../../../hooks/useParamSelector'; import { ed25519Str } from '../../../session/onions/onionPath'; import { CallManager } from '../../../session/utils'; +import { callTimeoutMs } from '../../../session/utils/calling/CallManager'; import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call'; import { Avatar, AvatarSize } from '../../Avatar'; import { SessionButton, SessionButtonColor } from '../SessionButton'; @@ -24,12 +25,10 @@ export const CallWindow = styled.div` border: var(--session-border); `; -const IncomingCallAvatatContainer = styled.div` +const IncomingCallAvatarContainer = styled.div` padding: 0 0 2rem 0; `; -const timeoutMs = 60000; - export const IncomingCallDialog = () => { const hasIncomingCall = useSelector(getHasIncomingCall); const incomingCallFromPubkey = useSelector(getHasIncomingCallFrom); @@ -42,11 +41,11 @@ export const IncomingCallDialog = () => { window.log.info( `call missed with ${ed25519Str( incomingCallFromPubkey - )} as dialog was not interacted with for ${timeoutMs} ms` + )} as the dialog was not interacted with for ${callTimeoutMs} ms` ); await CallManager.USER_rejectIncomingCallRequest(incomingCallFromPubkey); } - }, timeoutMs); + }, callTimeoutMs); } return () => { @@ -70,16 +69,16 @@ export const IncomingCallDialog = () => { } }; const from = useConversationUsername(incomingCallFromPubkey); - if (!hasIncomingCall) { + if (!hasIncomingCall || !incomingCallFromPubkey) { return null; } if (hasIncomingCall) { return ( - + - +
, dateBreak, unreadIndicator]; } - if (messageProps.message?.messageType === 'missed-call-notification') { - const msgProps = messageProps.message.props as PropsForMissedCallNotification; + if (messageProps.message?.messageType === 'call-notification') { + const msgProps = messageProps.message.props as PropsForCallNotification; - return [ - , - dateBreak, - unreadIndicator, - ]; + return [, dateBreak, unreadIndicator]; } if (!messageProps) { diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx index 591a55860..786e08349 100644 --- a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx @@ -75,7 +75,7 @@ export const SessionQuotedMessageComposition = () => { margin={'var(--margins-xs)'} > {window.i18n('replyingToMessage')} - + diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index 2079d9427..b83b15bd0 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -3,6 +3,9 @@ export type SessionIconType = | 'arrow' | 'bell' | 'brand' + | 'callIncoming' + | 'callMissed' + | 'callOutgoing' | 'caret' | 'chatBubble' | 'check' @@ -95,6 +98,24 @@ export const icons = { viewBox: '0 0 404.085 448.407', ratio: 1, }, + callIncoming: { + path: + 'M14.414 7l3.293-3.293a1 1 0 00-1.414-1.414L13 5.586V4a1 1 0 10-2 0v4.003a.996.996 0 00.617.921A.997.997 0 0012 9h4a1 1 0 100-2h-1.586zM2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z', + viewBox: '0 0 20 20', + ratio: 1, + }, + callOutgoing: { + path: + 'M17.924 2.617a.997.997 0 00-.215-.322l-.004-.004A.997.997 0 0017 2h-4a1 1 0 100 2h1.586l-3.293 3.293a1 1 0 001.414 1.414L16 5.414V7a1 1 0 102 0V3a.997.997 0 00-.076-.383zM2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z', + viewBox: '0 0 20 20', + ratio: 1, + }, + callMissed: { + path: + 'M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3zM16.707 3.293a1 1 0 010 1.414L15.414 6l1.293 1.293a1 1 0 01-1.414 1.414L14 7.414l-1.293 1.293a1 1 0 11-1.414-1.414L12.586 6l-1.293-1.293a1 1 0 011.414-1.414L14 4.586l1.293-1.293a1 1 0 011.414 0z', + viewBox: '0 0 20 20', + ratio: 1, + }, caret: { path: 'M127.5 191.25L255 63.75L0 63.75L127.5 191.25Z', viewBox: '-200 -200 640 640', diff --git a/ts/components/session/registration/RegistrationUserDetails.tsx b/ts/components/session/registration/RegistrationUserDetails.tsx index abf5905ba..f54aebf75 100644 --- a/ts/components/session/registration/RegistrationUserDetails.tsx +++ b/ts/components/session/registration/RegistrationUserDetails.tsx @@ -20,6 +20,7 @@ const DisplayNameInput = (props: { maxLength={MAX_USERNAME_LENGTH} onValueChanged={props.onDisplayNameChanged} onEnterPressed={props.handlePressEnter} + inputDataTestId="display-name-input" /> ); }; @@ -41,6 +42,7 @@ const RecoveryPhraseInput = (props: { enableShowHide={true} onValueChanged={props.onSeedChanged} onEnterPressed={props.handlePressEnter} + inputDataTestId="recovery-phrase-input" /> ); }; diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx index ed77d61ed..d23918749 100644 --- a/ts/components/session/registration/SignInTab.tsx +++ b/ts/components/session/registration/SignInTab.tsx @@ -39,6 +39,7 @@ const RestoreUsingRecoveryPhraseButton = (props: { onRecoveryButtonClicked: () = buttonType={SessionButtonType.BrandOutline} buttonColor={SessionButtonColor.Green} text={window.i18n('restoreUsingRecoveryPhrase')} + dataTestId="restore-using-recovery" /> ); }; diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx index e876bbc2b..7b8d87a34 100644 --- a/ts/components/session/settings/section/CategoryPrivacy.tsx +++ b/ts/components/session/settings/section/CategoryPrivacy.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; // tslint:disable-next-line: no-submodule-imports import useUpdate from 'react-use/lib/useUpdate'; +import { CallManager } from '../../../../session/utils'; import { sessionPassword, updateConfirmModal } from '../../../../state/ducks/modalDialog'; import { toggleMessageRequests } from '../../../../state/ducks/userConfig'; import { getIsMessageRequestsEnabled } from '../../../../state/selectors/userConfig'; @@ -23,6 +24,7 @@ const toggleCallMediaPermissions = async (triggerUIUpdate: () => void) => { onClickOk: async () => { await window.toggleCallMediaPermissionsTo(true); triggerUIUpdate(); + CallManager.onTurnedOnCallMediaPermissions(); }, onClickCancel: async () => { await window.toggleCallMediaPermissionsTo(false); diff --git a/ts/hooks/useDisableDrag.ts b/ts/hooks/useDisableDrag.ts new file mode 100644 index 000000000..b0844f4b3 --- /dev/null +++ b/ts/hooks/useDisableDrag.ts @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; + +/** + * This memoized function just returns a callback which can be used to disable the onDragStart event + */ +export const useDisableDrag = () => { + const cb = useCallback((e: any) => { + e.preventDefault(); + return false; + }, []); + + return cb; +}; diff --git a/ts/hooks/useEncryptedFileFetch.ts b/ts/hooks/useEncryptedFileFetch.ts index b71f51168..07d6c4bca 100644 --- a/ts/hooks/useEncryptedFileFetch.ts +++ b/ts/hooks/useEncryptedFileFetch.ts @@ -6,7 +6,7 @@ import { } from '../session/crypto/DecryptedAttachmentsManager'; import { perfEnd, perfStart } from '../session/utils/Performance'; -export const useEncryptedFileFetch = (url: string, contentType: string) => { +export const useEncryptedFileFetch = (url: string, contentType: string, isAvatar: boolean) => { // tslint:disable-next-line: no-bitwise const [urlToLoad, setUrlToLoad] = useState(''); const [loading, setLoading] = useState(false); @@ -16,7 +16,7 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => { async function fetchUrl() { perfStart(`getDecryptedMediaUrl-${url}`); - const decryptedUrl = await getDecryptedMediaUrl(url, contentType); + const decryptedUrl = await getDecryptedMediaUrl(url, contentType, isAvatar); perfEnd(`getDecryptedMediaUrl-${url}`, `getDecryptedMediaUrl-${url}`); if (mountedRef.current) { diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index e9ab569d4..c60f8b353 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -39,3 +39,16 @@ export function useOurConversationUsername() { export function useIsMe(pubkey?: string) { return pubkey && pubkey === UserUtils.getOurPubKeyStrFromCache(); } + +export function useIsClosedGroup(convoId?: string) { + return useSelector((state: StateType) => { + if (!convoId) { + return false; + } + const convo = state.conversations.conversationLookup[convoId]; + if (!convo) { + return false; + } + return (convo.isGroup && !convo.isPublic) || false; + }); +} diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index fad5af236..044aba6a3 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -331,7 +331,7 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) { return; } - const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG); + const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG, true); if (!decryptedAvatarUrl) { window.log.warn('Could not decrypt avatar stored locally..'); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 7a95a9979..20307bf5b 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -50,6 +50,7 @@ import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsMana import { IMAGE_JPEG } from '../types/MIME'; import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { getLatestTimestampOffset } from '../session/snode_api/SNodeAPI'; +import { createLastMessageUpdate } from '../types/Conversation'; export enum ConversationTypeEnum { GROUP = 'group', @@ -89,7 +90,7 @@ export interface ConversationAttributes { sessionRestoreSeen?: boolean; is_medium_group?: boolean; type: string; - avatarPointer?: any; + avatarPointer?: string; avatar?: any; /* Avatar hash is currently used for opengroupv2. it's sha256 hash of the base64 avatar data. */ avatarHash?: string; @@ -131,7 +132,7 @@ export interface ConversationAttributesOptionals { sessionRestoreSeen?: boolean; is_medium_group?: boolean; type: string; - avatarPointer?: any; + avatarPointer?: string; avatar?: any; avatarHash?: string; server?: any; @@ -900,9 +901,9 @@ export class ConversationModel extends Backbone.Model { const lastMessageJSON = lastMessageModel ? lastMessageModel.toJSON() : null; const lastMessageStatusModel = lastMessageModel ? lastMessageModel.getMessagePropStatus() - : null; - const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({ - currentTimestamp: this.get('active_at') || null, + : undefined; + const lastMessageUpdate = createLastMessageUpdate({ + currentTimestamp: this.get('active_at'), lastMessage: lastMessageJSON, lastMessageStatus: lastMessageStatusModel, lastMessageNotificationText: lastMessageModel ? lastMessageModel.getNotificationText() : null, @@ -1057,6 +1058,8 @@ export class ConversationModel extends Backbone.Model { ); const unreadCount = await this.getUnreadCount(); this.set({ unreadCount }); + this.updateLastMessage(); + await this.commit(); return model; } @@ -1487,7 +1490,7 @@ export class ConversationModel extends Backbone.Model { const avatarUrl = this.getAvatarPath(); const noIconUrl = 'images/session/session_icon_32.png'; if (avatarUrl) { - const decryptedAvatarUrl = await getDecryptedMediaUrl(avatarUrl, IMAGE_JPEG); + const decryptedAvatarUrl = await getDecryptedMediaUrl(avatarUrl, IMAGE_JPEG, true); if (!decryptedAvatarUrl) { window.log.warn('Could not decrypt avatar stored locally for getNotificationIcon..'); diff --git a/ts/models/message.ts b/ts/models/message.ts index 43b5a7f07..b4f0c7a9e 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -88,7 +88,7 @@ export class MessageModel extends Backbone.Model { const propsForGroupInvitation = this.getPropsForGroupInvitation(); const propsForGroupNotification = this.getPropsForGroupNotification(); const propsForTimerNotification = this.getPropsForTimerNotification(); - const isMissedCall = this.get('isMissedCall'); + const callNotificationType = this.get('callNotificationType'); const messageProps: MessageModelPropsWithoutConvoProps = { propsForMessage: this.getPropsForMessage(), }; @@ -105,9 +105,9 @@ export class MessageModel extends Backbone.Model { messageProps.propsForTimerNotification = propsForTimerNotification; } - if (isMissedCall) { - messageProps.propsForMissedCall = { - isMissedCall, + if (callNotificationType) { + messageProps.propsForCallNotification = { + notificationType: callNotificationType, messageId: this.id, receivedAt: this.get('received_at') || Date.now(), isUnread: this.isUnread(), @@ -239,6 +239,21 @@ export class MessageModel extends Backbone.Model { getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source) ); } + if (this.get('callNotificationType')) { + const displayName = getConversationController().getContactProfileNameOrShortenedPubKey( + this.get('conversationId') + ); + const callNotificationType = this.get('callNotificationType'); + if (callNotificationType === 'missed-call') { + return window.i18n('callMissed', displayName); + } + if (callNotificationType === 'started-call') { + return window.i18n('startedACall', displayName); + } + if (callNotificationType === 'answered-a-call') { + return window.i18n('answeredACall', displayName); + } + } return this.get('body'); } @@ -498,7 +513,7 @@ export class MessageModel extends Backbone.Model { return undefined; } - if (this.isDataExtractionNotification()) { + if (this.isDataExtractionNotification() || this.get('callNotificationType')) { return undefined; } diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index f880be5be..643b511d2 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -1,6 +1,6 @@ import _ from 'underscore'; import { v4 as uuidv4 } from 'uuid'; -import { PropsForMessageWithConvoProps } from '../state/ducks/conversations'; +import { CallNotificationType, PropsForMessageWithConvoProps } from '../state/ducks/conversations'; import { AttachmentTypeWithPath } from '../types/Attachment'; export type MessageModelType = 'incoming' | 'outgoing'; @@ -109,7 +109,7 @@ export interface MessageAttributes { */ isDeleted?: boolean; - isMissedCall?: boolean; + callNotificationType?: CallNotificationType; } export interface DataExtractionNotificationMsg { @@ -179,7 +179,7 @@ export interface MessageAttributesOptionals { direction?: any; messageHash?: string; isDeleted?: boolean; - isMissedCall?: boolean; + callNotificationType?: CallNotificationType; } /** diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index cbf0d4891..a8d099312 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -74,19 +74,19 @@ export async function handleCallMessage( if (type === SignalService.CallMessage.Type.ANSWER) { await removeFromCache(envelope); - await CallManager.handleCallTypeAnswer(sender, callMessage); + await CallManager.handleCallTypeAnswer(sender, callMessage, sentTimestamp); return; } if (type === SignalService.CallMessage.Type.ICE_CANDIDATES) { await removeFromCache(envelope); - await CallManager.handleCallTypeIceCandidates(sender, callMessage); + await CallManager.handleCallTypeIceCandidates(sender, callMessage, sentTimestamp); return; } await removeFromCache(envelope); // if this another type of call message, just add it to the manager - await CallManager.handleOtherCallTypes(sender, callMessage); + await CallManager.handleOtherCallTypes(sender, callMessage, sentTimestamp); } diff --git a/ts/session/crypto/DecryptedAttachmentsManager.ts b/ts/session/crypto/DecryptedAttachmentsManager.ts index 54b22b390..41f3c8433 100644 --- a/ts/session/crypto/DecryptedAttachmentsManager.ts +++ b/ts/session/crypto/DecryptedAttachmentsManager.ts @@ -11,20 +11,29 @@ import * as fse from 'fs-extra'; import { decryptAttachmentBuffer } from '../../types/Attachment'; import { DURATION } from '../constants'; -// FIXME. -// add a way to remove the blob when the attachment file path is removed (message removed?) -// do not hardcode the password -const urlToDecryptedBlobMap = new Map(); +const urlToDecryptedBlobMap = new Map< + string, + { decrypted: string; lastAccessTimestamp: number; forceRetain: boolean } +>(); const urlToDecryptingPromise = new Map>(); export const cleanUpOldDecryptedMedias = () => { const currentTimestamp = Date.now(); let countCleaned = 0; let countKept = 0; + let keptAsAvatars = 0; + window?.log?.info('Starting cleaning of medias blobs...'); for (const iterator of urlToDecryptedBlobMap) { - // if the last access is older than one hour, revoke the url and remove it. - if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) { + if ( + iterator[1].forceRetain && + iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.DAYS * 7 + ) { + // keep forceRetained items for at most 7 days + keptAsAvatars++; + } else if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) { + // if the last access is older than one hour, revoke the url and remove it. + URL.revokeObjectURL(iterator[1].decrypted); urlToDecryptedBlobMap.delete(iterator[0]); countCleaned++; @@ -32,10 +41,16 @@ export const cleanUpOldDecryptedMedias = () => { countKept++; } } - window?.log?.info(`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`); + window?.log?.info( + `Clean medias blobs: cleaned/kept/keptAsAvatars: ${countCleaned}:${countKept}:${keptAsAvatars}` + ); }; -export const getDecryptedMediaUrl = async (url: string, contentType: string): Promise => { +export const getDecryptedMediaUrl = async ( + url: string, + contentType: string, + isAvatar: boolean +): Promise => { if (!url) { return url; } @@ -50,11 +65,13 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr // if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it if (urlToDecryptedBlobMap.has(url)) { // refresh the last access timestamp so we keep the one being currently in use - const existingObjUrl = urlToDecryptedBlobMap.get(url)?.decrypted as string; + const existing = urlToDecryptedBlobMap.get(url); + const existingObjUrl = existing?.decrypted as string; urlToDecryptedBlobMap.set(url, { decrypted: existingObjUrl, lastAccessTimestamp: Date.now(), + forceRetain: existing?.forceRetain || false, }); // typescript does not realize that the has above makes sure the get is not undefined @@ -80,6 +97,7 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr urlToDecryptedBlobMap.set(url, { decrypted: obj, lastAccessTimestamp: Date.now(), + forceRetain: isAvatar, }); } urlToDecryptingPromise.delete(url); diff --git a/ts/session/utils/RingingManager.ts b/ts/session/utils/RingingManager.ts index f97dd1db3..5357afb3d 100644 --- a/ts/session/utils/RingingManager.ts +++ b/ts/session/utils/RingingManager.ts @@ -20,6 +20,10 @@ function startRinging() { void ringingAudio.play(); } +export function getIsRinging() { + return currentlyRinging; +} + export function setIsRinging(isRinging: boolean) { if (!currentlyRinging && isRinging) { startRinging(); diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index 6132a98df..02bfcd26c 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -5,6 +5,7 @@ import { KeyPair } from '../../../libtextsecure/libsignal-protocol'; import { PubKey } from '../types'; import { fromHexToArray, toHex } from './String'; import { getConversationController } from '../conversations'; +import { LokiProfile } from '../../types/Message'; export type HexKeyPair = { pubKey: string; @@ -93,13 +94,7 @@ export function setSignWithRecoveryPhrase(isLinking: boolean) { window.textsecure.storage.user.setSignWithRecoveryPhrase(isLinking); } -export interface OurLokiProfile { - displayName: string; - avatarPointer: string; - profileKey: Uint8Array | null; -} - -export function getOurProfile(): OurLokiProfile | undefined { +export function getOurProfile(): LokiProfile | undefined { try { // Secondary devices have their profile stored // in their primary device's conversation diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index b84c0e776..22517e615 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -1,7 +1,6 @@ import _ from 'lodash'; import { MessageUtils, ToastUtils, UserUtils } from '../'; import { getCallMediaPermissionsSettings } from '../../../components/session/settings/SessionSettings'; -import { getConversationById } from '../../../data/data'; import { MessageModelType } from '../../../models/messageType'; import { SignalService } from '../../../protobuf'; import { openConversationWithMessages } from '../../../state/ducks/conversations'; @@ -21,15 +20,18 @@ import { PubKey } from '../../types'; import { v4 as uuidv4 } from 'uuid'; import { PnServer } from '../../../pushnotification'; -import { setIsRinging } from '../RingingManager'; +import { getIsRinging, setIsRinging } from '../RingingManager'; import { getBlackSilenceMediaStream } from './Silence'; import { getMessageQueue } from '../..'; import { MessageSender } from '../../sending'; +import { DURATION } from '../../constants'; // tslint:disable: function-name export type InputItem = { deviceId: string; label: string }; +export const callTimeoutMs = 30000; + /** * This uuid is set only once we accepted a call or started one. */ @@ -88,10 +90,19 @@ export function removeVideoEventsListener(uniqueId: string) { callVideoListeners(); } +type CachedCallMessageType = { + type: SignalService.CallMessage.Type; + sdps: Array; + sdpMLineIndexes: Array; + sdpMids: Array; + uuid: string; + timestamp: number; +}; + /** * This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per call cache. */ -const callCache = new Map>>(); +const callCache = new Map>>(); let peerConnection: RTCPeerConnection | null; let dataChannel: RTCDataChannel | null; @@ -293,6 +304,8 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { if (sender?.track) { sender.track.enabled = false; } + const silence = getBlackSilenceMediaStream().getAudioTracks()[0]; + sender?.replaceTrack(silence); // do the same changes locally localStream?.getAudioTracks().forEach(t => { t.stop(); @@ -322,10 +335,14 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { return s.track?.kind === audioTrack.kind; }); window.log.info('replacing audio track'); - + // we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves) + // do the same changes locally + localStream?.getAudioTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); if (audioSender) { await audioSender.replaceTrack(audioTrack); - // we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves) } else { throw new Error('Failed to get sender for selectAudioInputByDeviceId '); } @@ -439,22 +456,38 @@ export async function USER_callRecipient(recipient: string) { return; } await updateConnectedDevices(); + const now = Date.now(); window?.log?.info(`starting call with ${ed25519Str(recipient)}..`); - window.inboxStore?.dispatch(startingCallWith({ pubkey: recipient })); + window.inboxStore?.dispatch( + startingCallWith({ + pubkey: recipient, + }) + ); if (peerConnection) { throw new Error('USER_callRecipient peerConnection is already initialized '); } currentCallUUID = uuidv4(); + const justCreatedCallUUID = currentCallUUID; peerConnection = createOrGetPeerConnection(recipient); // send a pre offer just to wake up the device on the remote side const preOfferMsg = new CallMessage({ - timestamp: Date.now(), + timestamp: now, type: SignalService.CallMessage.Type.PRE_OFFER, uuid: currentCallUUID, }); window.log.info('Sending preOffer message to ', ed25519Str(recipient)); - + const calledConvo = getConversationController().get(recipient); + await calledConvo?.addSingleMessage({ + conversationId: calledConvo.id, + source: UserUtils.getOurPubKeyStrFromCache(), + type: 'outgoing', + sent_at: now, + received_at: now, + expireTimer: 0, + callNotificationType: 'started-call', + unread: 0, + }); // we do it manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess // which is not the case for a pre offer message (the message only exists in memory) const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg); @@ -464,6 +497,17 @@ export async function USER_callRecipient(recipient: string) { await openMediaDevicesAndAddTracks(); setIsRinging(true); await createOfferAndSendIt(recipient); + + // close and end the call if callTimeoutMs is reached ans still not connected + global.setTimeout(async () => { + if (justCreatedCallUUID === currentCallUUID && getIsRinging()) { + window.log.info( + 'calling timeout reached. hanging up the call we started:', + justCreatedCallUUID + ); + await USER_hangup(recipient); + } + }, callTimeoutMs); } const iceCandidates: Array = new Array(); @@ -762,6 +806,18 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { await peerConnection.addIceCandidate(candicate); } } + const now = Date.now(); + const callerConvo = getConversationController().get(fromSender); + await callerConvo?.addSingleMessage({ + conversationId: callerConvo.id, + source: UserUtils.getOurPubKeyStrFromCache(), + type: 'incoming', + sent_at: now, + received_at: now, + expireTimer: 0, + callNotificationType: 'answered-a-call', + unread: 0, + }); await buildAnswerAndSendIt(fromSender); } @@ -809,6 +865,7 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) { if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) { closeVideoCall(); } + await addMissedCallMessage(fromSender, Date.now()); } async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { @@ -907,6 +964,20 @@ export function isCallRejected(uuid: string) { return rejectedCallUUIDS.has(uuid); } +function getCachedMessageFromCallMessage( + callMessage: SignalService.CallMessage, + envelopeTimestamp: number +) { + return { + type: callMessage.type, + sdps: callMessage.sdps, + sdpMLineIndexes: callMessage.sdpMLineIndexes, + sdpMids: callMessage.sdpMids, + uuid: callMessage.uuid, + timestamp: envelopeTimestamp, + }; +} + export async function handleCallTypeOffer( sender: string, callMessage: SignalService.CallMessage, @@ -920,6 +991,9 @@ export async function handleCallTypeOffer( window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID); if (!getCallMediaPermissionsSettings()) { + const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); + pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg); + await handleMissedCall(sender, incomingOfferTimestamp, true); return; } @@ -979,8 +1053,9 @@ export async function handleCallTypeOffer( } setIsRinging(true); } + const cachedMessage = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); - pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); + pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage); } catch (err) { window.log?.error(`Error handling offer message ${err}`); } @@ -991,7 +1066,7 @@ export async function handleMissedCall( incomingOfferTimestamp: number, isBecauseOfCallPermission: boolean ) { - const incomingCallConversation = await getConversationById(sender); + const incomingCallConversation = getConversationController().get(sender); setIsRinging(false); if (!isBecauseOfCallPermission) { ToastUtils.pushedMissedCall( @@ -1007,26 +1082,30 @@ export async function handleMissedCall( ); } + await addMissedCallMessage(sender, incomingOfferTimestamp); + return; +} + +async function addMissedCallMessage(callerPubkey: string, sentAt: number) { + const incomingCallConversation = getConversationController().get(callerPubkey); + await incomingCallConversation?.addSingleMessage({ - conversationId: incomingCallConversation.id, - source: sender, + conversationId: callerPubkey, + source: callerPubkey, type: 'incoming' as MessageModelType, - sent_at: incomingOfferTimestamp, + sent_at: sentAt, received_at: Date.now(), expireTimer: 0, - isMissedCall: true, + callNotificationType: 'missed-call', unread: 1, }); - incomingCallConversation?.updateLastMessage(); - - return; } function getOwnerOfCallUUID(callUUID: string) { for (const deviceKey of callCache.keys()) { for (const callUUIDEntry of callCache.get(deviceKey) as Map< string, - Array + Array >) { if (callUUIDEntry[0] === callUUID) { return deviceKey; @@ -1036,7 +1115,11 @@ function getOwnerOfCallUUID(callUUID: string) { return null; } -export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) { +export async function handleCallTypeAnswer( + sender: string, + callMessage: SignalService.CallMessage, + envelopeTimestamp: number +) { if (!callMessage.sdps || callMessage.sdps.length === 0) { window.log.warn('cannot handle answered message without signal description proto sdps'); return; @@ -1083,8 +1166,9 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe } else { window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`); } + const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp); - pushCallMessageToCallCache(sender, callMessageUUID, callMessage); + pushCallMessageToCallCache(sender, callMessageUUID, cachedMessage); if (!peerConnection) { window.log.info('handleCallTypeAnswer without peer connection. Dropping'); @@ -1114,7 +1198,8 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe export async function handleCallTypeIceCandidates( sender: string, - callMessage: SignalService.CallMessage + callMessage: SignalService.CallMessage, + envelopeTimestamp: number ) { if (!callMessage.sdps || callMessage.sdps.length === 0) { window.log.warn('cannot handle iceCandicates message without candidates'); @@ -1126,8 +1211,9 @@ export async function handleCallTypeIceCandidates( return; } window.log.info('handling callMessage ICE_CANDIDATES'); + const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp); - pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); + pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage); if (currentCallUUID && callMessage.uuid === currentCallUUID) { await addIceCandidateToExistingPeerConnection(callMessage); } @@ -1155,13 +1241,18 @@ async function addIceCandidateToExistingPeerConnection(callMessage: SignalServic } // tslint:disable-next-line: no-async-without-await -export async function handleOtherCallTypes(sender: string, callMessage: SignalService.CallMessage) { +export async function handleOtherCallTypes( + sender: string, + callMessage: SignalService.CallMessage, + envelopeTimestamp: number +) { const remoteCallUUID = callMessage.uuid; if (!remoteCallUUID || remoteCallUUID.length === 0) { window.log.warn('handleOtherCallTypes has no valid uuid'); return; } - pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); + const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp); + pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage); } function clearCallCacheFromPubkeyAndUUID(sender: string, callUUID: string) { @@ -1181,7 +1272,7 @@ function createCallCacheForPubkeyAndUUID(sender: string, uuid: string) { function pushCallMessageToCallCache( sender: string, uuid: string, - callMessage: SignalService.CallMessage + callMessage: CachedCallMessageType ) { createCallCacheForPubkeyAndUUID(sender, uuid); callCache @@ -1189,3 +1280,23 @@ function pushCallMessageToCallCache( ?.get(uuid) ?.push(callMessage); } + +/** + * Called when the settings of call media permissions is set to true from the settings page. + * Check for any recent offer and display it to the user if needed. + */ +export function onTurnedOnCallMediaPermissions() { + // this is not ideal as this might take the not latest sender from callCache + callCache.forEach((sender, key) => { + sender.forEach(msgs => { + for (const msg of msgs.reverse()) { + if ( + msg.type === SignalService.CallMessage.Type.OFFER && + Date.now() - msg.timestamp < DURATION.MINUTES * 1 + ) { + window.inboxStore?.dispatch(incomingCall({ pubkey: key })); + } + } + }); + }); +} diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 6124189f5..7971b4d44 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -17,8 +17,9 @@ import { QuotedAttachmentType } from '../../components/conversation/Quote'; import { perfEnd, perfStart } from '../../session/utils/Performance'; import { omit } from 'lodash'; -export type PropsForMissedCallNotification = { - isMissedCall: boolean; +export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call'; +export type PropsForCallNotification = { + notificationType: CallNotificationType; messageId: string; receivedAt: number; isUnread: boolean; @@ -30,7 +31,7 @@ export type MessageModelPropsWithoutConvoProps = { propsForTimerNotification?: PropsForExpirationTimer; propsForDataExtractionNotification?: PropsForDataExtractionNotification; propsForGroupNotification?: PropsForGroupUpdate; - propsForMissedCall?: PropsForMissedCallNotification; + propsForCallNotification?: PropsForCallNotification; }; export type MessageModelPropsWithConvoProps = SortedMessageModelProps & { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 7ee0fe576..85e21a34c 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -83,19 +83,6 @@ export const getSelectedConversationIsPublic = createSelector( } ); -const getConversationId = (_whatever: any, id: string) => id; - -export const getConversationById = createSelector( - getConversations, - getConversationId, - ( - state: ConversationsStateType, - convoId: string | undefined - ): ReduxConversationType | undefined => { - return convoId ? state.conversationLookup[convoId] : undefined; - } -); - export const getIsTypingEnabled = createSelector( getConversations, getSelectedConversationKey, @@ -190,7 +177,7 @@ export type MessagePropsType = | 'timer-notification' | 'regular-message' | 'unread-indicator' - | 'missed-call-notification'; + | 'call-notification'; export const getSortedMessagesTypesOfSelectedConversation = createSelector( getSortedMessagesOfSelectedConversation, @@ -257,14 +244,14 @@ export const getSortedMessagesTypesOfSelectedConversation = createSelector( }; } - if (msg.propsForMissedCall) { + if (msg.propsForCallNotification) { return { showUnreadIndicator: isFirstUnread, showDateBreak, message: { - messageType: 'missed-call-notification', + messageType: 'call-notification', props: { - ...msg.propsForMissedCall, + ...msg.propsForCallNotification, messageId: msg.propsForMessage.id, }, }, diff --git a/ts/test/types/Conversation_test.ts b/ts/test/types/Conversation_test.ts index fb3d27f6f..884f66e13 100644 --- a/ts/test/types/Conversation_test.ts +++ b/ts/test/types/Conversation_test.ts @@ -1,4 +1,5 @@ import { assert } from 'chai'; +import { LastMessageStatusType } from '../../state/ducks/conversations'; import * as Conversation from '../../types/Conversation'; import { IncomingMessage } from '../../types/Message'; @@ -9,8 +10,8 @@ describe('Conversation', () => { const input = {}; const expected = { lastMessage: '', - lastMessageStatus: null, - timestamp: null, + lastMessageStatus: undefined, + timestamp: undefined, }; const actual = Conversation.createLastMessageUpdate(input); @@ -21,7 +22,7 @@ describe('Conversation', () => { it('should update last message text and timestamp', () => { const input = { currentTimestamp: 555, - lastMessageStatus: 'read', + lastMessageStatus: 'read' as LastMessageStatusType, lastMessage: { type: 'outgoing', conversationId: 'foo', @@ -32,7 +33,7 @@ describe('Conversation', () => { }; const expected = { lastMessage: 'New outgoing message', - lastMessageStatus: 'read', + lastMessageStatus: 'read' as LastMessageStatusType, timestamp: 666, }; @@ -60,7 +61,7 @@ describe('Conversation', () => { }; const expected = { lastMessage: 'Last message before expired', - lastMessageStatus: null, + lastMessageStatus: undefined, timestamp: 555, }; diff --git a/ts/types/Conversation.ts b/ts/types/Conversation.ts index 9f3289e3d..e8de28a70 100644 --- a/ts/types/Conversation.ts +++ b/ts/types/Conversation.ts @@ -1,9 +1,10 @@ +import { LastMessageStatusType } from '../state/ducks/conversations'; import { Message } from './Message'; interface ConversationLastMessageUpdate { lastMessage: string; - lastMessageStatus: string | null; - timestamp: number | null; + lastMessageStatus: LastMessageStatusType; + timestamp: number | undefined; } export const createLastMessageUpdate = ({ @@ -14,14 +15,14 @@ export const createLastMessageUpdate = ({ }: { currentTimestamp?: number; lastMessage?: Message; - lastMessageStatus?: string; + lastMessageStatus?: LastMessageStatusType; lastMessageNotificationText?: string; }): ConversationLastMessageUpdate => { if (!lastMessage) { return { lastMessage: '', - lastMessageStatus: null, - timestamp: null, + lastMessageStatus: undefined, + timestamp: undefined, }; } @@ -35,7 +36,7 @@ export const createLastMessageUpdate = ({ return { lastMessage: lastMessageNotificationText || '', - lastMessageStatus: lastMessageStatus || null, - timestamp: newTimestamp || null, + lastMessageStatus: lastMessageStatus || undefined, + timestamp: newTimestamp || undefined, }; }; diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 82d41b78a..487833518 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -51,6 +51,6 @@ type MessageSchemaVersion5 = Partial< export type LokiProfile = { displayName: string; - avatarPointer: string; + avatarPointer?: string; profileKey: Uint8Array | null; }; diff --git a/ts/util/attachmentsUtil.ts b/ts/util/attachmentsUtil.ts index 5cdb01f4e..fc6f04d12 100644 --- a/ts/util/attachmentsUtil.ts +++ b/ts/util/attachmentsUtil.ts @@ -151,7 +151,7 @@ export const saveAttachmentToDisk = async ({ messageSender: string; conversationId: string; }) => { - const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType); + const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType, false); save({ attachment: { ...attachment, url: decryptedUrl }, document, From 651b1c437615a9f2e25e40009e112e72f25c58da Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 30 Nov 2021 14:46:06 +1100 Subject: [PATCH 55/70] Fetch translations (#2056) * show missed-call,started-call and answered call notification in chat * fix types for createLastMessageUpdate * show incoming dialog if we have a pending call when enable call receptio * simplify a bit the avatar component * move disableDrag to a custom hook * speed up hash colors of avatarPlaceHolders * fixup text selection and double click reply on message * keep avatar decoded items longer before releasing memory * add incoming/outgoing/missed call notification also, merge that notification with the timer and group notification component * hangup call if no answer after 30sec * refactor SessionInput using hook + add testid field for recovery * disable message request feature flag for now * fix merge issue * force loading screen to be black instead of white for our dark theme user's eyes safety * add type for i18n to run update after crowdin fetch with tools/updateI18nKeysType.py * update to latest translations --- _locales/ar/messages.json | 200 ++-- _locales/bg/messages.json | 68 +- _locales/ca/messages.json | 60 +- _locales/cs/messages.json | 56 +- _locales/da/messages.json | 56 +- _locales/de/messages.json | 90 +- _locales/el/messages.json | 56 +- _locales/eo/messages.json | 56 +- _locales/es/messages.json | 116 +-- _locales/es_419/messages.json | 56 +- _locales/et/messages.json | 56 +- _locales/fa/messages.json | 70 +- _locales/fi/messages.json | 56 +- _locales/fil/messages.json | 56 +- _locales/fr/messages.json | 66 +- _locales/he/messages.json | 56 +- _locales/hi/messages.json | 56 +- _locales/hr/messages.json | 56 +- _locales/hu/messages.json | 56 +- _locales/id/messages.json | 56 +- _locales/it/messages.json | 84 +- _locales/ja/messages.json | 56 +- _locales/ka/messages.json | 106 ++- _locales/km/messages.json | 56 +- _locales/kn/messages.json | 56 +- _locales/ko/messages.json | 56 +- _locales/lt/messages.json | 74 +- _locales/mk/messages.json | 56 +- _locales/nb/messages.json | 56 +- _locales/nl/messages.json | 538 +++++------ _locales/no/messages.json | 900 +++++++++--------- _locales/pa/messages.json | 56 +- _locales/pl/messages.json | 84 +- _locales/pt_BR/messages.json | 86 +- _locales/pt_PT/messages.json | 56 +- _locales/ro/messages.json | 56 +- _locales/ru/messages.json | 106 ++- _locales/si/messages.json | 76 +- _locales/sk/messages.json | 56 +- _locales/sl/messages.json | 56 +- _locales/sq/messages.json | 56 +- _locales/sr/messages.json | 56 +- _locales/sv/messages.json | 56 +- _locales/ta/messages.json | 56 +- _locales/th/messages.json | 56 +- _locales/tr/messages.json | 146 +-- _locales/uk/messages.json | 162 ++-- _locales/vi/messages.json | 56 +- _locales/zh_CN/messages.json | 130 +-- _locales/zh_TW/messages.json | 250 ++--- tools/updateI18nKeysType.py | 28 + ts/components/Avatar.tsx | 2 +- .../conversation/ConversationHeader.tsx | 2 +- .../DataExtractionNotification.tsx | 4 +- .../conversation/GroupNotification.tsx | 17 +- .../conversation/StagedLinkPreview.tsx | 2 +- .../message/ClickToTrustSender.tsx | 14 +- .../notification-bubble/CallNotification.tsx | 5 +- ts/components/dialog/DeleteAccountModal.tsx | 7 +- ts/components/dialog/InviteContactsDialog.tsx | 2 +- .../dialog/SessionPasswordDialog.tsx | 13 +- .../dialog/UpdateGroupMembersDialog.tsx | 9 +- .../dialog/UpdateGroupNameDialog.tsx | 6 +- .../session/calling/IncomingCallDialog.tsx | 2 +- .../conversation/SessionRightPanel.tsx | 2 +- ts/components/session/menu/Menu.tsx | 13 +- .../session/registration/SignInTab.tsx | 2 +- .../session/registration/SignUpTab.tsx | 2 +- .../session/settings/SessionSettings.tsx | 14 +- ts/interactions/messageInteractions.ts | 4 +- ts/models/conversation.ts | 4 +- ts/models/message.ts | 49 +- ts/session/utils/Toast.tsx | 4 +- ts/state/selectors/conversations.ts | 2 +- ts/types/LocalizerKeys.ts | 465 +++++++++ ts/types/Util.ts | 4 +- ts/window.d.ts | 3 +- 77 files changed, 3302 insertions(+), 2471 deletions(-) create mode 100755 tools/updateI18nKeysType.py create mode 100644 ts/types/LocalizerKeys.ts diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index 0a3c77c47..16e784594 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -1,63 +1,63 @@ { - "privacyPolicy": "Terms & Privacy Policy", - "copyErrorAndQuit": "Copy error and quit", - "unknown": "Unknown", - "databaseError": "Database Error", - "mainMenuFile": "&File", - "mainMenuEdit": "&Edit", - "mainMenuView": "&View", - "mainMenuWindow": "&Window", - "mainMenuHelp": "&Help", - "appMenuHide": "Hide", - "appMenuHideOthers": "Hide Others", - "appMenuUnhide": "Show All", - "appMenuQuit": "Quit Session", - "editMenuUndo": "Undo", - "editMenuRedo": "Redo", - "editMenuCut": "Cut", - "editMenuCopy": "Copy", - "editMenuPaste": "Paste", - "editMenuPasteAndMatchStyle": "Paste and Match Style", + "privacyPolicy": "الشروط وسياسة الخصوصية", + "copyErrorAndQuit": "نسخ الخطأ والخروج", + "unknown": "مجهول", + "databaseError": "خطأ في قاعدة البيانات", + "mainMenuFile": "&ملف", + "mainMenuEdit": "&تعديل", + "mainMenuView": "&عرض", + "mainMenuWindow": "&نافذة", + "mainMenuHelp": "&مساعدة", + "appMenuHide": "إخفاء", + "appMenuHideOthers": "إخفاء الآخرين", + "appMenuUnhide": "إظهار الكل", + "appMenuQuit": "إنهاء سِيشَن", + "editMenuUndo": "تراجع", + "editMenuRedo": "إعادة", + "editMenuCut": "قص", + "editMenuCopy": "نسخ", + "editMenuPaste": "لصق", + "editMenuPasteAndMatchStyle": "لصق ومطابقة النمط", "editMenuDelete": "أحذف ", "editMenuSelectAll": "Select All", - "windowMenuClose": "Close Window", - "windowMenuMinimize": "Minimize", - "windowMenuZoom": "Zoom", - "windowMenuBringAllToFront": "Bring All to Front", - "viewMenuResetZoom": "Actual Size", - "viewMenuZoomIn": "Zoom In", - "viewMenuZoomOut": "Zoom Out", - "viewMenuToggleFullScreen": "Toggle Full Screen", - "viewMenuToggleDevTools": "Toggle Developer Tools", - "contextMenuNoSuggestions": "No Suggestions", - "openGroupInvitation": "Open group invitation", - "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", - "enterSessionIDOrONSName": "Enter Session ID or ONS name", + "windowMenuClose": "أغلق النافذة", + "windowMenuMinimize": "صغِّر", + "windowMenuZoom": "تكبير", + "windowMenuBringAllToFront": "أحضر الجميع للأمام", + "viewMenuResetZoom": "الحجم الحقيقي", + "viewMenuZoomIn": "تكبير", + "viewMenuZoomOut": "تصغير", + "viewMenuToggleFullScreen": "الانتقال إلى ملء الشاشة", + "viewMenuToggleDevTools": "الانتقال إلى أدوات المطور", + "contextMenuNoSuggestions": "لا اقتراحات", + "openGroupInvitation": "دعوة المجموعة المفتوحة", + "joinOpenGroupAfterInvitationConfirmationTitle": "الانضمام إلى $roomName$؟", + "joinOpenGroupAfterInvitationConfirmationDesc": "هل أنت متأكد من أنك تريد الانضمام إلى المجموعة المفتوحة $roomName$؟", + "enterSessionIDOrONSName": "أدخل معرف سِيشَن أو اسم ONS", "loading": "جاري التحميل", - "optimizingApplication": "Optimizing application...", - "done": "Done", + "optimizingApplication": "تحسين التطبيق...", + "done": "تم", "me": "أنا", "view": "معاينة ", "youLeftTheGroup": "لقد غادرت المجموعة ", - "youGotKickedFromGroup": "You were removed from the group.", - "unreadMessage": "Unread Message", - "unreadMessages": "Unread Messages", + "youGotKickedFromGroup": "تمت إزالتك من المجموعة.", + "unreadMessage": "رسالة غير مقروءة", + "unreadMessages": "الرسائل غير المقروءة", "debugLogExplanation": "هذه السجلاّت سيتم نشرها علنا ليتمكن المساهمون من معاينتها. ربما ترغب فى تعديلها أو التحقق منها قبل إرسالها. ", - "debugLogError": "Something went wrong with the upload! Please consider manually adding your log to the bug you file.", + "debugLogError": "حدث خطأ ما في الرفع! الرجاء النظر في إضافة السجل الخاص بك يدوياً إلى الخطأ الذي ستقوم بإيداعه.", "reportIssue": "بلّغ عن مشكل", "gotIt": "واضح", "submit": "إرسال", - "markAllAsRead": "Mark All as Read", - "incomingError": "Error handling incoming message", - "media": "Media", + "markAllAsRead": "تحديد الكل كمقروء", + "incomingError": "خطأ في معالجة الرسالة الواردة", + "media": "الوسائط", "mediaEmptyState": "You don’t have any media in this conversation", - "documents": "Documents", + "documents": "الوثائق", "documentsEmptyState": "You don’t have any documents in this conversation", - "today": "Today", - "yesterday": "Yesterday", + "today": "اليوم", + "yesterday": "الأمس", "thisWeek": "This Week", - "thisMonth": "This Month", + "thisMonth": "هذا الشهر", "voiceMessage": "رسالة صوتية", "dangerousFileType": "Attachment type not allowed for security reasons", "stagedPreviewThumbnail": "Draft thumbnail link preview for $domain$", @@ -71,8 +71,9 @@ "offline": "غير متصل بالإنترنت ", "checkNetworkConnection": "الرجاء التحقق من الاتصال بالشبكة", "attemptingReconnection": "جاري اعادة الاتصال خلال $reconnect_duration_in_seconds$ ثواني", - "submitDebugLog": "Debug log", + "submitDebugLog": "سجل تصحيح الأخطاء", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "خطأ", "delete": "أحذف ", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "أحذف هذه الرسالة", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "حذف الرسائل", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "مِن", "to": "to", "sent": "أُرسلت", @@ -124,14 +126,6 @@ "groupMembers": "أفراد المجموعة", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "حذف الرسالة", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "حذف الرسائل", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "حذف المحادثة بصفة نهائية ؟", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -144,21 +138,22 @@ "addACaption": "Add a caption...", "copy": "Copy", "copySessionID": "Copy Session ID", - "copyOpenGroupURL": "Copy Group's URL", + "copyOpenGroupURL": "نسخ رابط المجموعة", "save": "حفظ", - "saved": "Saved", - "permissions": "Permissions", + "saveLogToDesktop": "Save log to desktop", + "saved": "تم الحفظ", + "permissions": "الصلاحيات", "general": "عام", - "tookAScreenshot": "$name$ took a screenshot", - "savedTheFile": "Media saved by $name$", - "linkPreviewsTitle": "Send Link Previews", - "linkPreviewDescription": "Previews are supported for most urls", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", - "spellCheckTitle": "Spell Check", - "spellCheckDescription": "Enable spell check of text entered in message composition box", - "spellCheckDirty": "You must restart Session to apply your new settings", + "tookAScreenshot": "قام $name$ بتصوير الشاشة", + "savedTheFile": "قام $name$ بحفظ الوسائط", + "linkPreviewsTitle": "إرسال معاينات الروابط", + "linkPreviewDescription": "المعاينات مدعومة لمعظم الروابط", + "linkPreviewsConfirmMessage": "لن يكون لديك حماية كاملة للبيانات الوصفية عند إرسال معاينات الرابط.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", + "spellCheckTitle": "التدقيق الإملائي", + "spellCheckDescription": "تفعيل التحقق الإملائي على النص المدخل في مربع تكوين الرسالة", + "spellCheckDirty": "يجب عليك إعادة تشغيل سِيشَن لتطبيق إعداداتك الجديدة", "notifications": "الإشعارات", "readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).", "readReceiptSettingTitle": "Read Receipts", @@ -234,20 +229,20 @@ "autoUpdateNewVersionTitle": "Session update available", "autoUpdateNewVersionMessage": "There is a new version of Session available.", "autoUpdateNewVersionInstructions": "Press Restart Session to apply the updates.", - "autoUpdateRestartButtonLabel": "Restart Session", - "autoUpdateLaterButtonLabel": "Later", - "autoUpdateDownloadButtonLabel": "Download", + "autoUpdateRestartButtonLabel": "قم بإعادة تشغيل سِيشَن", + "autoUpdateLaterButtonLabel": "لاحقاً", + "autoUpdateDownloadButtonLabel": "تنزيل", "autoUpdateDownloadedMessage": "The new update has been downloaded.", - "autoUpdateDownloadInstructions": "Would you like to download the update?", + "autoUpdateDownloadInstructions": "هل تريد تنزيل التحديث؟", "leftTheGroup": "$name$ left the group", - "multipleLeftTheGroup": "$name$ left the group", - "updatedTheGroup": "Group updated", + "multipleLeftTheGroup": "$name$ غادر المجموعة", + "updatedTheGroup": "تم تحديث المجموعة", "titleIsNow": "Group name has been set to '$name$'", "joinedTheGroup": "$name$ joined the group", "multipleJoinedTheGroup": "$names$ joined the group", - "kickedFromTheGroup": "$name$ was removed from the group.", + "kickedFromTheGroup": "تمت إزالة $name$ من المجموعة.", "multipleKickedFromTheGroup": "$name$ were removed from the group.", - "blockUser": "Block", + "blockUser": "حَظْر", "unblockUser": "Unblock", "unblocked": "Unblocked", "blocked": "Blocked", @@ -272,10 +267,10 @@ "failedToAddAsModerator": "Failed to add user as moderator", "failedToRemoveFromModerator": "Failed to remove user from the moderator list", "copyMessage": "Copy message text", - "selectMessage": "Select message", - "editGroup": "Edit group", - "editGroupName": "Edit group name", - "updateGroupDialogTitle": "Updating $name$...", + "selectMessage": "حدد الرسالة", + "editGroup": "تعديل المجموعة", + "editGroupName": "تعديل اسم المجموعة", + "updateGroupDialogTitle": "جارٍ تحديث $name$...", "showRecoveryPhrase": "Recovery Phrase", "yourSessionID": "Your Session ID", "setAccountPasswordTitle": "Set Account Password", @@ -285,9 +280,9 @@ "removeAccountPasswordTitle": "Remove Account Password", "removeAccountPasswordDescription": "Remove the password associated with your account", "enterPassword": "Please enter your password", - "confirmPassword": "Confirm password", + "confirmPassword": "تأكيد كلمة المرور", "pasteLongPasswordToastTitle": "The clipboard content exceeds the maximum password length of $max_pwd_len$ characters.", - "showRecoveryPhrasePasswordRequest": "Please enter your password", + "showRecoveryPhrasePasswordRequest": "الرجاء إدخال كلمة المرور الخاصة بك", "recoveryPhraseSavePromptMain": "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don't give it to anyone.", "invalidOpenGroupUrl": "Invalid URL", "copiedToClipboard": "Copied to clipboard", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 4b2b02ed2..9f018a7ce 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -29,18 +29,18 @@ "viewMenuZoomOut": "Уменьшить", "viewMenuToggleFullScreen": "Переключить полный экран", "viewMenuToggleDevTools": "Переключить инструменты разработчика", - "contextMenuNoSuggestions": "No Suggestions", + "contextMenuNoSuggestions": "Няма предложения", "openGroupInvitation": "Open group invitation", "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", "enterSessionIDOrONSName": "Enter Session ID or ONS name", "loading": "Загрузка...", "optimizingApplication": "Оптимизация приложения...", - "done": "Done", + "done": "Готово", "me": "Я", "view": "Просмотреть", "youLeftTheGroup": "Вы покинули группу.", - "youGotKickedFromGroup": "You were removed from the group.", + "youGotKickedFromGroup": "Бяхте премахнат от тази група", "unreadMessage": "Unread Message", "unreadMessages": "Unread Messages", "debugLogExplanation": "Этот журнал отладки будет открыто опубликован для разработчиков. Вы можете проверить и изменить его перед отправкой.", @@ -73,13 +73,14 @@ "attemptingReconnection": "Попытка переподключения через $reconnect_duration_in_seconds$ секунд", "submitDebugLog": "Журнал отладки", "debugLog": "Журнал отладки", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Перейти к заметкам о релизе", "goToSupportPage": "Перейти на страницу поддержки", "menuReportIssue": "Сообщить о проблеме", "about": "О Session", "speech": "Речь", "show": "Показать", - "sessionMessenger": "Session", + "sessionMessenger": "Сесия", "search": "Поиск", "noSearchResults": "Результаты не найдены для \"$searchTerm$\"", "conversationsHeader": "Беседы", @@ -105,17 +106,18 @@ "cannotUpdateDetail": "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", "ok": "Добре", "cancel": "Отменить", - "close": "Close", + "close": "Затвори", "continue": "Продолжить", "error": "Ошибка", "delete": "Удалить", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Вы уверены? Нажав 'удалить' вы навсегда удалите данное сообщение только с этого устройства.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Удалить сообщение", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Удалить сообщения", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "От:", "to": "Кому:", "sent": "Отправлено", @@ -124,14 +126,6 @@ "groupMembers": "Участники группы", "moreInformation": "Больше информации", "resend": "Отправить ещё раз", - "deleteMessage": "Удалить Сообщение", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Удалить сообщения", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Удалить этот разговор без возможности восстановления?", "clearAllData": "Очистить все данные", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Сохранить", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Разрешения", "general": "Общие", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Отправлять Предпросмотр Ссылки", "linkPreviewDescription": "Previews are supported for most urls.", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Разрешить доступ к камере и микрофону", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Проверка орфографии", "spellCheckDescription": "Включить проверку орфографии текста, введенного в поле создания сообщения", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -258,7 +253,7 @@ "userUnbanFailed": "Unban failed!", "banUser": "Ban User", "banUserConfirm": "Are you sure you want to ban user?", - "banUserAndDeleteAll": "Ban and Delete All", + "banUserAndDeleteAll": "Забрани и изтрий всички", "banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?", "userBanned": "User banned successfully", "userBanFailed": "Ban failed!", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ca/messages.json b/_locales/ca/messages.json index 29963944d..306638bdc 100644 --- a/_locales/ca/messages.json +++ b/_locales/ca/messages.json @@ -41,8 +41,8 @@ "view": "Mostra", "youLeftTheGroup": "Heu abandonat el grup", "youGotKickedFromGroup": "T'han eliminat del grup.", - "unreadMessage": "Unread Message", - "unreadMessages": "Unread Messages", + "unreadMessage": "Missatge sense llegir", + "unreadMessages": "Missatges sense llegir", "debugLogExplanation": "Aquest registre es penjarà públicament perquè el vegin els col·laboradors. Podeu examinar-lo i editar-lo abans d'enviar-lo.", "debugLogError": "S'ha produït un error amb la càrrega! Considereu afegir manualment el registre a l'error que envieu.", "reportIssue": "Informeu d'un error", @@ -73,6 +73,7 @@ "attemptingReconnection": "S'intentarà de reconnectar en $reconnect_duration_in_seconds$ segons", "submitDebugLog": "Registre de depuració", "debugLog": "Registre de depuració", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Vés a les notes de versió", "goToSupportPage": "Vés a la pàgina de suport", "menuReportIssue": "Informeu d'un error", @@ -109,13 +110,14 @@ "continue": "Continuar", "error": "Error", "delete": "Suprimeix", - "deletePublicWarning": "Estàs segur? D'aquesta manera, se suprimirà aquest missatge de manera permanent per a tothom d'aquest grup obert.", - "deleteMultiplePublicWarning": "Estàs segur? Això eliminarà aquests missatges de manera permanent per a tothom d'aquest grup obert.", - "deleteWarning": "N'esteu segur? Si feu clic en «Suprimeix» aquest missatge s'esborrarà només en aquest aparell.", - "deleteMultipleWarning": "N'esteu segur? Si feu clic en «Suprimeix» aquest missatge s'esborrarà només en aquest dispositiu.", "messageDeletionForbidden": "No teniu autorització per esborrar els missatges d'altres", - "deleteThisMessage": "Suprimeix aquest missatge", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Suprimeix els missatges", "deleted": "Eliminat", + "messageDeletedPlaceholder": "This message has been deleted", "from": "De", "to": "a", "sent": "Enviament", @@ -124,14 +126,6 @@ "groupMembers": "Membres del grup", "moreInformation": "Més informació", "resend": "Reenviar", - "deleteMessage": "Suprimeix el missatge", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Suprimeix els missatges", - "deleteMessageForEveryone": "Suprimeix el missatge per a tothom", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Suprimeix el missatge per a tothom", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Voleu suprimir aquesta conversa de forma permanent?", "clearAllData": "Esborrar totes les dades", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copia l'ID de Session", "copyOpenGroupURL": "Copia l'URL del grup", "save": "Desa", + "saveLogToDesktop": "Save log to desktop", "saved": "Desat", "permissions": "Permisos", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Envia previsualitzacions d'enllaços", "linkPreviewDescription": "Les previsualitzacions són compatibles amb la majoria d’Urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Micròfon i càmera", - "mediaPermissionsDescription": "Permet l'accés a la càmera i el micròfon", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Revisar Ortografia", "spellCheckDescription": "Activa la comprovació ortogràfica del text introduït en el quadre d'edició de missatges", "spellCheckDirty": "Heu de reiniciar Session per aplicar la vostra nova configuració", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 4538d0a32..0f563d8b2 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Pokus o znovupřipojení za $reconnect_duration_in_seconds$ sekund", "submitDebugLog": "Ladící log", "debugLog": "Ladící log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Přejít na poznámky k vydání", "goToSupportPage": "Přejít na stránky podpory", "menuReportIssue": "Nahlásit problém", @@ -109,13 +110,14 @@ "continue": "Pokračovat", "error": "Chyba", "delete": "Vymazat", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Jste si jisti? Kliknutím na 'smazat' permanentně vymažete zprávu z tohoto zařízení.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Smazat tuto zprávu", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Smazat zprávy", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Od", "to": "to", "sent": "Odeslána", @@ -124,14 +126,6 @@ "groupMembers": "Členové skupiny", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Smazat zprávu", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Smazat zprávy", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Trvale smazat tuto konverzaci?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Uložit", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Oprávnění", "general": "Obecné", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofon a fotoaparát", - "mediaPermissionsDescription": "Umožnit přístup ke kameře a mikrofonu", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Kontrola pravopisu", "spellCheckDescription": "Kontrolovat pravopis při psaní zpráv", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/da/messages.json b/_locales/da/messages.json index 47af48ce3..9996b20e9 100644 --- a/_locales/da/messages.json +++ b/_locales/da/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Forsøger at genoprette forbindelse på $reconnect_duration_in_seconds$ sekunder", "submitDebugLog": "Debug log", "debugLog": "Debug log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Gå til udgivelsesnoter", "goToSupportPage": "Gå til supportsiden", "menuReportIssue": "Indmeld en fejl", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Fejl", "delete": "Slet", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Er du sikker? Hvis du klikker på 'Slet', fjernes denne meddelelse permanent og kun fra denne enhed.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Slet besked", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Slet beskeder", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Fra", "to": "til", "sent": "Sendt", @@ -124,14 +126,6 @@ "groupMembers": "Gruppemedlemmer", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Slet besked", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Slet beskeder", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Slet samtale permanent?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Gem", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Tilladelser", "general": "Generelt", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Tillad adgang til kamera og mikrofon", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Aktiver stavekontrol af beskeder", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/de/messages.json b/_locales/de/messages.json index a5616e7a2..e15eb2828 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Neuer Verbindungsversuch in $reconnect_duration_in_seconds$ Sekunden", "submitDebugLog": "Diagnoseprotokoll", "debugLog": "Diagnoseprotokoll", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Versionshinweise", "goToSupportPage": "Support", "menuReportIssue": "Problem melden", @@ -109,13 +110,14 @@ "continue": "Weiter", "error": "Fehler", "delete": "Löschen", - "deletePublicWarning": "Sind Sie sicher? Dies wird diese Nachricht für alle Teilnehmer dieser offenen Gruppe dauerhaft entfernen.", - "deleteMultiplePublicWarning": "Sind Sie sicher? Dies wird diese Nachrichten für alle Teilnehmer dieser offenen Gruppe dauerhaft entfernen.", - "deleteWarning": "Bist du sicher? Das Anklicken von »Löschen« wird diese Nachricht unwiderruflich von nur diesem Gerät entfernen.", - "deleteMultipleWarning": "Sind Sie sicher? Ein Klick auf „Löschen“ löscht diese Nachrichten dauerhaft von diesem Gerät.", "messageDeletionForbidden": "Ihnen fehlt die Berechtigung, Nachrichten anderer Teilnehmer zu löschen", - "deleteThisMessage": "Nachrichten löschen", + "deleteJustForMe": "Nur für mich löschen", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Alle Nachrichten löschen", "deleted": "Gelöscht", + "messageDeletedPlaceholder": "Die Nachricht wurde gelöscht", "from": "Von:", "to": "An:", "sent": "Gesendet", @@ -124,17 +126,9 @@ "groupMembers": "Gruppenmitglieder", "moreInformation": "Mehr Informationen", "resend": "Erneut Senden", - "deleteMessage": "Nachrichten Löschen", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Alle Nachrichten löschen", - "deleteMessageForEveryone": "Nachricht für alle löschen", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Nachrichten für alle löschen", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Soll diese Unterhaltung unwiderruflich gelöscht werden?", "clearAllData": "Alle Daten löschen", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Dies wird all deine Nachrichten und Kontakte permanent löschen.", "deleteContactConfirmation": "Möchten Sie diese Unterhaltung wirklich löschen?", "quoteThumbnailAlt": "Miniaturbild aus zitierter Nachricht", "imageAttachmentAlt": "Bildanhang", @@ -146,6 +140,7 @@ "copySessionID": "Session-ID kopieren", "copyOpenGroupURL": "Gruppen-URL kopieren", "save": "Speichern", + "saveLogToDesktop": "Save log to desktop", "saved": "Gespeichert", "permissions": "Berechtigungen", "general": "Allgemein", @@ -153,9 +148,9 @@ "savedTheFile": "Medien gespeichert von $name$", "linkPreviewsTitle": "Link-Vorschauen Senden", "linkPreviewDescription": "Vorschau wird für die meisten URLs unterstützt", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofon und Kamera", - "mediaPermissionsDescription": "Zugriff auf Kamera und Mikrofon erlauben", + "linkPreviewsConfirmMessage": "Beim Senden von Link-Vorschauen sind Ihre Metadaten nicht vollständig geschützt.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Rechtschreibprüfung", "spellCheckDescription": "Rechtschreibprüfung für im Nachrichteneingabefeld eingegebenen Text aktivieren", "spellCheckDirty": "Sie müssen Session neu starten, um die neuen Einstellungen zu übernehmen", @@ -340,7 +335,7 @@ "onlyAdminCanRemoveMembersDesc": "Nur der Ersteller der Gruppe kann Benutzer entfernen", "createAccount": "Konto Erstellen", "signIn": "Einloggen", - "startInTrayTitle": "Start in Tray", + "startInTrayTitle": "Im Infobereich starten", "startInTrayDescription": "Wollen sie Session als minimierten Anwendung starten ", "yourUniqueSessionID": "Das ist Ihre Session ID.", "allUsersAreRandomly...": "Ihre Session ID ist die eindeutige Adresse, unter der Personen Sie über Session kontaktieren können. Ihre Session ID ist nicht mit Ihrer realen Identität verbunden, völlig anonym und von Natur aus privat.", @@ -421,40 +416,51 @@ "unpinConversation": "Unterhaltung abnehmen", "pinConversationLimitTitle": "Limit für angeheftete Unterhaltungen", "pinConversationLimitToastDescription": "Du kannst höchstens $number$ Unterhaltungen anheften", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Erste ungelesene Nachricht ist oben", "sendRecoveryPhraseTitle": "Wiederherstellungsphrase zusenden", "sendRecoveryPhraseMessage": "Mit der Wiederherstellungsphrase kann auf deinen Account zugegriffen werden. Bist du dir sicher das du sie dir zusenden lassen möchtest?", "dialogClearAllDataDeletionFailedTitle": "Daten nicht gelöscht", "dialogClearAllDataDeletionFailedDesc": "Die Daten wurden aufgrund eines unbekannten Fehlers nicht gelöscht. Möchten Sie Ihre Daten nur von diesem Gerät löschen?", "dialogClearAllDataDeletionFailedTitleQuestion": "Möchten Sie Daten nur von diesem Gerät löschen?", - "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", + "dialogClearAllDataDeletionFailedMultiple": "Diese Serviceknoten haben die Daten nicht gelöscht: $snodes$", "dialogClearAllDataDeletionQuestion": "Möchten Sie nur dieses Gerät entfernen, oder Ihr Konto vollständig löschen?", - "deviceOnly": "Device Only", + "deviceOnly": "Nur Geräte", "entireAccount": "Gesamtes Konto", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "areYouSureDeleteDeviceOnly": "Bist du dir sicher dass du deine Gerätedaten löschen willst?", + "areYouSureDeleteEntireAccount": "Sind Sie sicher, dass Sie Ihr gesamtes Konto, einschließlich der Daten im Netzwerk, löschen möchten?", + "iAmSure": "Ich bin mir sicher", "recoveryPhraseSecureTitle": "Du bist fast fertig!", "recoveryPhraseRevealMessage": "Sichern Sie Ihr Konto, indem Sie Ihre Wiederherstellungsphrase speichern. Zeigen Sie Ihre Wiederherstellungsphrase an und speichern Sie diese sicher.", "recoveryPhraseRevealButtonText": "Wiederherstellungsphrase anzeigen", "notificationSubtitle": "Benachrichtigungen - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Nimm unsere Session Umfrage an", + "goToOurSurvey": "Zur Umfrage", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Akzeptieren", + "decline": "Ablehnen", + "endCall": "Anruf beenden", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Neuer Anruf konnte nicht gestartet werden", + "callMissed": "Entgangener Anruf von $name$", + "callMissedTitle": "Anruf verpasst", + "noCameraFound": "Keine Kamera gefunden", + "noAudioInputFound": "Keine Audioeingabe gefunden", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 14a619e9a..7dc6fd854 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Απόπειρα επανασύνδεσης σε $reconnect_duration_in_seconds$ δευτερόλεπτα", "submitDebugLog": "Αρχείο καταγραφής αποσφαλμάτωσης", "debugLog": "Αρχείο καταγραφής αποσφαλμάτωσης", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Μετάβαση στις Σημειώσεις Έκδοσης", "goToSupportPage": "Μετάβαση στη Σελίδα Υποστήριξης", "menuReportIssue": "Αναφορά Σφάλματος", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Σφάλμα", "delete": "Διαγραφή", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Είστε σίγουρος/η; Πατώντας \"διαγραφή\", αυτό το μήνυμα θα καταργηθεί οριστικά από αυτή την συσκευή.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Διαγραφή αυτού του μηνύματος", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Διαγραφή Μηνυμάτων", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Από", "to": "to", "sent": "Στάλθηκε", @@ -124,14 +126,6 @@ "groupMembers": "Μέλη ομάδας", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Διαγραφή Μηνύματος", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Διαγραφή Μηνυμάτων", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Οριστική διαγραφή αυτής της συνομιλίας;", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Αποθήκευση", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Άδειες", "general": "Γενικά", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Να επιτρέπεται η πρόσβαση στην κάμερα και μικρόφωνο", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Ενεργοποίηση ορθογραφικού ελέγχου του κειμένου που εισάγεται στο παράθυρο σύνθεσης μηνύματος", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json index afcb9d0c8..be2235cb4 100644 --- a/_locales/eo/messages.json +++ b/_locales/eo/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Provo rekonekti post $reconnect_duration_in_seconds$ sekundoj", "submitDebugLog": "Sencimiga protokolo", "debugLog": "Sencimiga protokolo", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Iri al eldonaj notoj", "goToSupportPage": "Iri al helppaĝo", "menuReportIssue": "Raporti problemon", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Eraro", "delete": "Forigi", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Ĉu vi pricertas? Per alklako de „Forigi“, tiu mesaĝo porĉiame foriĝos nur de ĉi tiu aparato.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Forigi tiun ĉi mesaĝon", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Forigi mesaĝojn", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "El", "to": "al", "sent": "Sendita", @@ -124,14 +126,6 @@ "groupMembers": "Grupanoj", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Forigi mesaĝon", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Forigi mesaĝojn", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Ĉu porĉiame forigi tiun ĉi tutan interparolon?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Kopii Session ID-on", "copyOpenGroupURL": "Copy Group's URL", "save": "Konservi", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permesoj", "general": "Ĝenerala", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Permesi aliron al la fotilo kaj la mikrofono", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Ŝalti literumilon de teksto entajpita en mesaĝa verkejo", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index e09fa2879..8c9b89828 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1,5 +1,5 @@ { - "privacyPolicy": "Términos y política de privacidad", + "privacyPolicy": "Aviso Legal y Política de Privacidad", "copyErrorAndQuit": "Copiar fallo y cerrar Session", "unknown": "Desconocido", "databaseError": "Fallo en la base de datos", @@ -73,6 +73,7 @@ "attemptingReconnection": "Intentando volver a conectar en $reconnect_duration_in_seconds$ segundos", "submitDebugLog": "Registro de depuración (log)", "debugLog": "Registro de depuración", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Ir a las notas de versión", "goToSupportPage": "Ir a la página de soporte técnico", "menuReportIssue": "Informar de un problema", @@ -109,13 +110,14 @@ "continue": "Continuar", "error": "Fallo", "delete": "Eliminar", - "deletePublicWarning": "¿Estas seguro? El mensaje será eliminado permanentemente para todos en el grupo.", - "deleteMultiplePublicWarning": "¿Estás seguro? Estos mensajes se eliminarán para todos en el grupo permanentemente.", - "deleteWarning": "¿Estás segura? Al hacer clic en 'Eliminar' se borrará el mensaje permanentemente de este (y solamente este) dispositivo.", - "deleteMultipleWarning": "¿Estas seguro? Esto borrará el mensaje solo para ti permanentemente.", "messageDeletionForbidden": "No tienes permiso de borrar mensajes de otros", - "deleteThisMessage": "Eliminar mensaje", + "deleteJustForMe": "Eliminar solo para mí", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Eliminar mensajes", "deleted": "Eliminado", + "messageDeletedPlaceholder": "Este mensaje se ha eliminado", "from": "Desde:", "to": "Para:", "sent": "Enviado", @@ -124,17 +126,9 @@ "groupMembers": "Miembros del grupo", "moreInformation": "Más detalles", "resend": "Reenviar", - "deleteMessage": "Eliminar Mensaje", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Eliminar mensajes", - "deleteMessageForEveryone": "Borrar mensaje para todos", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Borrar mensajes para todos", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "¿Eliminar este chat permanentemente?", "clearAllData": "Borrar todos los datos", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Esto eliminará permanentemente tu ID de Session, incluyendo todos los mensajes, sesiones y contactos.", "deleteContactConfirmation": "¿Seguro que quieres eliminar esta conversación?", "quoteThumbnailAlt": "Miniatura de una foto como cita de un mensaje", "imageAttachmentAlt": "Imagen adjunta al mensaje", @@ -146,6 +140,7 @@ "copySessionID": "Copiad ID de Session", "copyOpenGroupURL": "Copiar URL del grupo", "save": "Guardar", + "saveLogToDesktop": "Save log to desktop", "saved": "Guardado", "permissions": "Permisos", "general": "General", @@ -153,9 +148,9 @@ "savedTheFile": "$name$ guardó el archivo", "linkPreviewsTitle": "Enviar Previsualizaciones", "linkPreviewDescription": "Previews are supported for most urls.", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Micrófono y Cámara", - "mediaPermissionsDescription": "Permitir acesso a cámara y micrófono", + "linkPreviewsConfirmMessage": "No tendrás una privacidad completa de metadatos al enviar previsualizaciones de enlaces.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Revisión ortográfica", "spellCheckDescription": "Comprobar la ortografía al escribir el mensaje", "spellCheckDirty": "Debes reiniciar Session para aplicar las nuevas configuraciones", @@ -203,7 +198,7 @@ "timerOption_1_week": "1 semana", "disappearingMessages": "Desaparición de mensajes", "changeNickname": "Cambiar nombre de usuario", - "clearNickname": "Clear nickname", + "clearNickname": "Borrar nombre de usuario", "nicknamePlaceholder": "Nuevo nombre de usuario", "changeNicknameMessage": "Ingresa un nombre de usuario", "timerOption_0_seconds_abbreviated": "inactivo", @@ -279,10 +274,10 @@ "showRecoveryPhrase": "Frase de recuperación", "yourSessionID": "Tu ID de Session", "setAccountPasswordTitle": "Establecer contraseña de la cuenta", - "setAccountPasswordDescription": "Require password to unlock Session’s screen. You can still receive message notifications while Screen Lock is enabled. Session’s notification settings allow you to customize information that is displayed", + "setAccountPasswordDescription": "Requiere contraseña para desbloquear la pantalla de Session. Seguirás recibiendo notificaciones mientras el bloqueo de pantalla está activado. Los ajustes de notificación de Session te permiten personalizar la información que se muestra", "changeAccountPasswordTitle": "Cambiar la Contraseña de la Cuenta", - "changeAccountPasswordDescription": "Change your password", - "removeAccountPasswordTitle": "Remove Account Password", + "changeAccountPasswordDescription": "Cambia tu contraseña", + "removeAccountPasswordTitle": "Eliminar contraseña de cuenta", "removeAccountPasswordDescription": "Eliminar la contraseña asociada a tu cuenta", "enterPassword": "Por favor, introduce tu contraseña", "confirmPassword": "Confirmar contraseña", @@ -297,7 +292,7 @@ "setPassword": "Establecer Contraseña", "changePassword": "Cambiar Contraseña", "removePassword": "Eliminar Contraseña", - "maxPasswordAttempts": "Invalid Password. Would you like to reset the database?", + "maxPasswordAttempts": "Contraseña inválida. ¿Restablecer la base de datos?", "typeInOldPassword": "Por favor, escribe tu contraseña antigua", "invalidOldPassword": "La contraseña antigua es inválida", "invalidPassword": "Contraseña inválida", @@ -306,7 +301,7 @@ "setPasswordInvalid": "Las contraseñas no coinciden", "changePasswordInvalid": "La contraseña antigua que ingresaste es incorrecta", "removePasswordInvalid": "Contraseña incorrecta", - "setPasswordTitle": "Set Password", + "setPasswordTitle": "Establecer Contraseña", "changePasswordTitle": "Contraseña Cambiada", "removePasswordTitle": "Contraseña Eliminada", "setPasswordToastDescription": "Tu contraseña ha sido establecida. Por favor, manténgala segura.", @@ -317,12 +312,12 @@ "connectingToServer": "Conectando ...", "connectToServerSuccess": "Conectado con exito al grupo abierto", "setPasswordFail": "Error al establecer la contraseña", - "passwordLengthError": "Password must be between 6 and 64 characters long", + "passwordLengthError": "La contraseña debe tener entre 6 y 12 caracteres de longitud", "passwordTypeError": "La contraseña debe ser una cadena de texto", - "passwordCharacterError": "Password must only contain letters, numbers and symbols", + "passwordCharacterError": "La contraseña solo debe contener letras, números y símbolos", "remove": "Eliminar", "invalidSessionId": "ID de Session no válida", - "invalidPubkeyFormat": "Invalid Pubkey Format", + "invalidPubkeyFormat": "Formato de Clave Pública inválido", "emptyGroupNameError": "Por favor, ingresa un nombre de grupo", "editProfileModalTitle": "Perfil", "groupNamePlaceholder": "Nombre Del Grupo", @@ -340,8 +335,8 @@ "onlyAdminCanRemoveMembersDesc": "Sólo el creador del grupo puede eliminar usuarios", "createAccount": "Create Account", "signIn": "Iniciar sesión", - "startInTrayTitle": "Start in Tray", - "startInTrayDescription": "Start Session as a minified app ", + "startInTrayTitle": "Iniciar en la bandeja del sistema", + "startInTrayDescription": "Iniciar Session como una aplicación minificada ", "yourUniqueSessionID": "Saluda a tu ID de Session", "allUsersAreRandomly...": "Tu ID de Session es la dirección única que las personas pueden usar para contactarte en Session. Por diseño, tu ID de Session es totalmente anónima y privada, sin vínculo con tu identidad real.", "getStarted": "Comenzar", @@ -421,40 +416,51 @@ "unpinConversation": "Desanclar conversación", "pinConversationLimitTitle": "Límite de conversaciones ancladas", "pinConversationLimitToastDescription": "Solo puedes anclar $number$ conversaciones", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "El primer mensaje no leído está arriba", "sendRecoveryPhraseTitle": "Enviando Frase de Recuperación", "sendRecoveryPhraseMessage": "Estás intentando enviar tu frase de recuperación, que puede utilizarse para acceder a tu cuenta. ¿Estás seguro de que deseas enviar este mensaje?", "dialogClearAllDataDeletionFailedTitle": "Datos no eliminados", - "dialogClearAllDataDeletionFailedDesc": "Data not deleted with an unknown error. Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedTitleQuestion": "Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", + "dialogClearAllDataDeletionFailedDesc": "Los datos no se eliminaron por un error desconocido. ¿Quieres eliminarlos solo de este dispositivo?", + "dialogClearAllDataDeletionFailedTitleQuestion": "¿Quieres eliminar sólo los datos de este dispositivo?", + "dialogClearAllDataDeletionFailedMultiple": "Datos no eliminados por los siguientes nodos de servicio: $snodes$", "dialogClearAllDataDeletionQuestion": "¿Quieres borrar sólo este dispositivo o eliminar toda tu cuenta?", "deviceOnly": "Solamente en el dispositivo", - "entireAccount": "Entire Account", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "entireAccount": "Toda la cuenta", + "areYouSureDeleteDeviceOnly": "¿Estás seguro de querer eliminar sólo los datos del dispositivo?", + "areYouSureDeleteEntireAccount": "¿Estás seguro de querer eliminar la cuenta, incluyendo los datos de red?", + "iAmSure": "Estoy seguro", "recoveryPhraseSecureTitle": "¡Ya casi has terminado!", "recoveryPhraseRevealMessage": "Protege tu cuenta al guardar la frase de recuperación. Revela tu frase de recuperación y guárdala de forma segura para protegerla.", "recoveryPhraseRevealButtonText": "Mostrar frase de recuperación", "notificationSubtitle": "Notificaciones - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Realiza nuestra encuesta de Session", + "goToOurSurvey": "Ir a la encuesta", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Aceptar", + "decline": "Rechazar", + "endCall": "Finalizar llamada", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "No se puede iniciar la llamada", + "callMissed": "Llamada perdida de $name$", + "callMissedTitle": "Llamada perdida", + "noCameraFound": "Cámara no encontrada", + "noAudioInputFound": "No se encontró dispositivo de sonido", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/es_419/messages.json b/_locales/es_419/messages.json index d1c1b2121..dd42dff66 100644 --- a/_locales/es_419/messages.json +++ b/_locales/es_419/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continuar", "error": "Error", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Enviado", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Guardar", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/et/messages.json b/_locales/et/messages.json index a597c9d0a..cf272e2a6 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "$reconnect_duration_in_seconds$ sekundi pärast proovitakse uuesti ühenduda.", "submitDebugLog": "Silumisinfo", "debugLog": "Silumisinfo", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Mine väljalaskemärkmete juurde", "goToSupportPage": "Mine kasutajatoelehele", "menuReportIssue": "Teavita probleemist", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Tõrge", "delete": "Kustuta", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Kas sa oled kindel? Klõpsates \"Kustuta\" eemaldatakse see sõnum ainult sellest seadmest.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Kustuta see sõnum", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Kustuta sõnumid", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Saatja", "to": "to", "sent": "Saadetud", @@ -124,14 +126,6 @@ "groupMembers": "Grupi liikmed", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Kustuta sõnum", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Kustuta sõnumid", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Kas kustutada see vestlus jäädavalt?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Salvesta", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Õigused", "general": "Üldine", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Luba ligipääs kaamerale ja mikrofonile", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Luba sõnumite kasti teksti õigekirja kontroll", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/fa/messages.json b/_locales/fa/messages.json index 2be29dcdf..4b3d87652 100644 --- a/_locales/fa/messages.json +++ b/_locales/fa/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "گزارش خطاء", "debugLog": "گزارش خطا", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "رفتن به یادداشت های ریلیز", "goToSupportPage": "رفتن به صفحه پشتیبانی", "menuReportIssue": "گزارش مشکل", @@ -109,13 +110,14 @@ "continue": "ادامه", "error": "خطا", "delete": "پاک کن", - "deletePublicWarning": "آیا مطمئن هستید می‌خواهید این پیام را برای همگان در این گروه به صورت دائمی پاک کنید؟", - "deleteMultiplePublicWarning": "آیا مطمئن هستید می‌خواهید این پیام‌ها را برای همگان در این گروه به صورت دائمی پاک کنید؟", - "deleteWarning": "آیا مطمئن هستید؟ کلیک روی 'حذف' این پیام را برای همیشه از روی این دستگاه حذف خواهد کرد.", - "deleteMultipleWarning": "آیا مطمئن هستید؟ با کلیک بر روی 'حذف'، این پیام‌ها فقط از این دستگاه حذف خواهد شد.", "messageDeletionForbidden": "شما دسترسی برای حذف پیام دیگران را ندارید", - "deleteThisMessage": "این پیام را حذف کن", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "حذف پیام‌ها", "deleted": "حذف شده", + "messageDeletedPlaceholder": "This message has been deleted", "from": "از", "to": "to", "sent": "ارسال شد", @@ -124,17 +126,9 @@ "groupMembers": "اعضای گروه", "moreInformation": "اطلاعات بیشتر", "resend": "ارسال مجدد", - "deleteMessage": "حذف پیام", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "حذف پیام‌ها", - "deleteMessageForEveryone": "حذف پیام برای همگان", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "حذف پیام‌ها برای همگان", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "آیا می‌خواهید این گفتگو را برای همیشه حذف کنید؟", "clearAllData": "پاک‌سازی همه داده‌ها", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "این کار برای همیشه پیام ها و مخاطبین شما را حذف می کند .", "deleteContactConfirmation": "آیا مطمئن هستید که می‌خواهید این مکالمه را حذف کنید؟", "quoteThumbnailAlt": "پیش‌نمایش تصویر از پیام نقل قول شده", "imageAttachmentAlt": "تصویر پیوست شده به پیام", @@ -146,6 +140,7 @@ "copySessionID": "کپی شناسه‌ی Session", "copyOpenGroupURL": "کپی آدرس گروه", "save": "ذخیره", + "saveLogToDesktop": "Save log to desktop", "saved": "ذخیره شده", "permissions": "دسترسی ها", "general": "عمومی", @@ -153,9 +148,9 @@ "savedTheFile": "رسانه توسط $name$ ذخیره شد", "linkPreviewsTitle": "ارسال پیش‌نمایش لینک", "linkPreviewDescription": "پیش‌نمایش‌ها برای اکثر آدرس‌ها پشتیبانی می‌شوند", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "میکروفون و دوربین", - "mediaPermissionsDescription": "اجازه دسترسی به دوربین و میکروفون", + "linkPreviewsConfirmMessage": "زمانی کی پیش نمایش لینک ها را می فرستید و دریافت می کنید فراداده یا متا دیتای شما محافظت نخواهد شد.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "بررسی املا", "spellCheckDescription": "فعال سازی کنترل املاء متن وارد شده در باکس پیام نویسی", "spellCheckDirty": "برای اعمال تنظیمات جدید نیاز است Session را دوباره راه‌اندازی کنید", @@ -238,7 +233,7 @@ "autoUpdateLaterButtonLabel": "بعدا", "autoUpdateDownloadButtonLabel": "دانلود", "autoUpdateDownloadedMessage": "The new update has been downloaded.", - "autoUpdateDownloadInstructions": "Would you like to download the update?", + "autoUpdateDownloadInstructions": "آیا میخواهید بروز رسانی را دانلود کنید ؟", "leftTheGroup": "$name$ گروه را ترک کرد", "multipleLeftTheGroup": "$name$ گروه را ترک کرد", "updatedTheGroup": "گروه آپدیت شد", @@ -252,13 +247,13 @@ "unblocked": "رفع مسدودی", "blocked": "مسدود شده", "blockedSettingsTitle": "مخاطبین مسدود شده", - "unbanUser": "Unban User", + "unbanUser": "غیر مسدود کردن کاربر", "unbanUserConfirm": "Are you sure you want to unban user?", - "userUnbanned": "User unbanned successfully", - "userUnbanFailed": "Unban failed!", + "userUnbanned": "کاربر با موفقیت غیر مستود شد", + "userUnbanFailed": "غیر مستود کردن شکست خورد!", "banUser": "مسدود کردن کاربر", "banUserConfirm": "Are you sure you want to ban user?", - "banUserAndDeleteAll": "Ban and Delete All", + "banUserAndDeleteAll": "مستود کردن کاربر و حذف کردن داده های آن", "banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?", "userBanned": "User banned successfully", "userBanFailed": "مسدود کردن شکست خورد!", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "محدودیت سنجاق کردن گفتگوها", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "اعلان ها - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index b332af056..efd0bf2d6 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Yritetään uudelleen $reconnect_duration_in_seconds$ sekunnin kuluttua", "submitDebugLog": "Virheenkorjausloki", "debugLog": "Virheenkorjausloki", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Siirry julkaisutietoihin", "goToSupportPage": "Siirry tukisivulle", "menuReportIssue": "Ilmoita ongelmasta", @@ -109,13 +110,14 @@ "continue": "Jatka", "error": "Virhe", "delete": "Poista", - "deletePublicWarning": "Oletko varma? Tämä poistaa pysyvästi tämän viestin kaikilta avoimen ryhmän jäseniltä.", - "deleteMultiplePublicWarning": "Oletko varma? Tämä poistaa kaikki nämä viesti kaikilta jäseniltä avoimessa ryhmässä.", - "deleteWarning": "Oletko varma? Tämä poistaa pysyvästi tämän viestin ainoastaan tästä laitteesta.", - "deleteMultipleWarning": "Oletko varma? Painamalla 'Poista', kaikki nämä viestit poistetaan pysyvästi ainoastaan tältä laitteelta.", "messageDeletionForbidden": "Sinulla ei ole oikeutta poistaa toisten käyttäjien viestejä", - "deleteThisMessage": "Poista tämä viesti", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Poista viestit", "deleted": "Poistettu", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Lähettäjä", "to": "vastaanottaja", "sent": "Lähetetty", @@ -124,14 +126,6 @@ "groupMembers": "Ryhmän jäsenet", "moreInformation": "Lisätietoja", "resend": "Uudelleenlähetä", - "deleteMessage": "Poista viesti", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Poista viestit", - "deleteMessageForEveryone": "Poista viesti kaikilta", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Poista viesti kaikilta", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Poistetaanko tämä keskustelu pysyvästi?", "clearAllData": "Poista kaikki data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Kopioi Session ID", "copyOpenGroupURL": "Kopioi ryhmän URL -osoite", "save": "Tallenna", + "saveLogToDesktop": "Save log to desktop", "saved": "Tallennettu", "permissions": "Käyttöoikeudet", "general": "Yleistä", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Lähetä linkin esikatselu", "linkPreviewDescription": "Esikatselut ovat tuettuja useimmilla URL-osoitteilla", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofoni ja kamera", - "mediaPermissionsDescription": "Myönnä kameran ja mikrofonin käyttöoikeus", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Oikeinkirjoituksen tarkistus", "spellCheckDescription": "Ota käyttöön viestilaatikkoon kirjoitetun tekstin oikeinkirjoituksen tarkistus", "spellCheckDirty": "Sinun täytyy uudelleenkäynnistää Session -sovellus ottaaksesi käyttöön asetusten muutokset", @@ -421,6 +416,7 @@ "unpinConversation": "Irroita keskustelu", "pinConversationLimitTitle": "Kiinnitettyjen keskuteluiden raja", "pinConversationLimitToastDescription": "Voit kiinnittää enintään $number$ keskustelua", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Viimeisin lukematon viesti on ylimpänä", "sendRecoveryPhraseTitle": "Lähetetään palautuslausetta", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/fil/messages.json b/_locales/fil/messages.json index fa57f7c4c..8428cbf5a 100644 --- a/_locales/fil/messages.json +++ b/_locales/fil/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Error", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Sent", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Save", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 8c510ae57..390a1c329 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Tentative de reconnexion $reconnect_duration_in_seconds$ secondes", "submitDebugLog": "Journal de débogage", "debugLog": "Journal de débogage", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Accéder aux notes de mise à jour", "goToSupportPage": "Accéder à la page d’assistance", "menuReportIssue": "Signaler un problème", @@ -109,13 +110,14 @@ "continue": "Continuer", "error": "Erreur", "delete": "Supprimer", - "deletePublicWarning": "Êtes-vous sûr? Cela supprimera ce message pour tous les membres de ce groupe public.", - "deleteMultiplePublicWarning": "Êtes-vous sûr? Cela supprimera ces messages pour tous les membres de ce groupe public.", - "deleteWarning": "Êtes-vous certain ? Cliquer sur Supprimer éliminera définitivement ce message de cet appareil seulement.", - "deleteMultipleWarning": "Êtes-vous sûr? En cliquant sur 'Supprimer' cela supprimera ces messages sur cet appareil uniquement.", "messageDeletionForbidden": "Vous n'êtes pas autorisé à supprimer les messages des autres utilisateurs.", - "deleteThisMessage": "Supprimer le message", + "deleteJustForMe": "Supprimer juste pour moi", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Supprimer les messages", "deleted": "Supprimé", + "messageDeletedPlaceholder": "Ce message a été supprimé", "from": "De :", "to": "À :", "sent": "Envoyé", @@ -124,17 +126,9 @@ "groupMembers": "Membres du groupe", "moreInformation": "Plus d’informations", "resend": "Renvoyer", - "deleteMessage": "Supprimer le message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Supprimer les messages", - "deleteMessageForEveryone": "Supprimer ce message pour tout le monde", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Supprimer ces messages pour tout le monde", - "deleteForEveryone": "Supprimer ce message", "deleteConversationConfirmation": "Supprimer définitivement cette conversation ?", "clearAllData": "Effacer toutes les données", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Cela supprimera définitivement vos messages, vos sessions et vos contacts.", "deleteContactConfirmation": "Voulez-vous vraiment supprimer cette conversation ?", "quoteThumbnailAlt": "Imagette du message cité", "imageAttachmentAlt": "Image jointe au message", @@ -146,6 +140,7 @@ "copySessionID": "Copier la Session ID", "copyOpenGroupURL": "Copier l'URL de Group", "save": "Enregistrer", + "saveLogToDesktop": "Save log to desktop", "saved": "Enregistré", "permissions": "Autorisations", "general": "Général", @@ -153,9 +148,9 @@ "savedTheFile": "$name$ a enregistré le média", "linkPreviewsTitle": "Envoyer des aperçus de liens", "linkPreviewDescription": "Les aperçus sont pris en charge pour la plupart des URLs", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone et caméra", - "mediaPermissionsDescription": "Autoriser l’accès à la caméra et au micro", + "linkPreviewsConfirmMessage": "Vous n'aurez pas une protection complète des métadonnées en envoyant des aperçu de liens.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Vérification orthographique", "spellCheckDescription": "Activer la vérification de l’orthographe du texte saisi dans la fenêtre de rédaction des messages", "spellCheckDirty": "Vous devez redémarrer Session pour appliquer ces changements.", @@ -421,6 +416,7 @@ "unpinConversation": "Désépingler la conversation", "pinConversationLimitTitle": "Limite de conversation épinglée", "pinConversationLimitToastDescription": "Vous pouvez seulement épingler $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Le premier message non lu se situe au-dessus", "sendRecoveryPhraseTitle": "Envoyer la phrase de récupération", "sendRecoveryPhraseMessage": "Vous essayer actuellement d’envoyer votre phrase de récupération, qui peut être utilisée pour accéder a votre compte. Êtes-vous sûre de vouloir envoyer ce message ?", @@ -431,30 +427,40 @@ "dialogClearAllDataDeletionQuestion": "Souhaitez-vous effacer seulement sur cet appareil ou supprimer l'ensemble de votre compte ?", "deviceOnly": "Appareil uniquement", "entireAccount": "L’ensemble du compte", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "areYouSureDeleteDeviceOnly": "Êtes-vous sûr de vouloir supprimer uniquement les données de votre appareil ?", + "areYouSureDeleteEntireAccount": "Êtes-vous sûr de vouloir supprimer l'ensemble de votre compte, y compris les données du réseau?", + "iAmSure": "Je suis sûr(e)", "recoveryPhraseSecureTitle": "Vous avez presque terminé !", "recoveryPhraseRevealMessage": "Sécurisez votre compte en enregistrant votre phrase de récupération. Afficher votre phrase de récupération puis stockez-la en toute sécurité pour la sécuriser.", "recoveryPhraseRevealButtonText": "Afficher la phrase de récupération", "notificationSubtitle": "Paramètres des notifications", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 26c43b29d..d311805ba 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "מנסה להתחבר מחדש תוך $reconnect_duration_in_seconds$ שניות", "submitDebugLog": "יומן תקלים", "debugLog": "יומן תקלים", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "לך אל הערות שחרור", "goToSupportPage": "לך אל דף התמיכה", "menuReportIssue": "דווח על סוגייה", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "שגיאה", "delete": "מחק", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "האם אתה בטוח? לחיצה על 'מחק' תסיר לצמיתות הודעה זו ממכשיר זה בלבד.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "מחק הודעה זו", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "מחק הודעות", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "מאת", "to": "אל", "sent": "נשלח", @@ -124,14 +126,6 @@ "groupMembers": "חברי קבוצה", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "מחק הודעה", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "מחק הודעות", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "האם למחוק לצמיתות שיחה זו?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "שמור", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "הרשאות", "general": "כללי", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "התר גישה אל מצלמה ומיקרופון", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "אפשר בדיקת איות של מלל המוכנס בתיבת חיבור הודעה", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index 282083fad..58a601ac2 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "फिर से कनेक्ट करने का प्रयास करना$reconnect_duration_in_seconds$सेकंड", "submitDebugLog": "डीबग लॉग", "debugLog": "लॉग को डीबग करें", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "रिलीज़ नोट्स पे जाइए", "goToSupportPage": "सहायता पेज पर जाएँ", "menuReportIssue": "समस्या की रिपोर्ट करें…", @@ -109,13 +110,14 @@ "continue": "जारी रखें", "error": " गलती", "delete": "हटाना", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "यह संदेश हटाएं", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "संदेश हटाएँ", "deleted": "हटा दिया गया", + "messageDeletedPlaceholder": "This message has been deleted", "from": "किस से", "to": "to", "sent": "भेज दिया", @@ -124,14 +126,6 @@ "groupMembers": "समूह के सदस्य", "moreInformation": "अधिक जानकारी", "resend": "फिर से भेजें", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "संदेश हटाएँ", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "इस वार्तालाप को स्थायी रूप से हटाएं?", "clearAllData": "सभी डेटा हटाएं", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "संरक्षित करें", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index 462f5c6e5..0d65edffe 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Greška", "delete": "Obriši", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Obriši ovu poruku", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Izbriši poruku", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Od", "to": "to", "sent": "Poslano", @@ -124,14 +126,6 @@ "groupMembers": "Članovi grupe", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Izbriši poruku", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Trajno obrisati ovaj razgovor?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Spremi", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "Općenito", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index bba3c4ce4..ae5f1f942 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Újracsatlakozási kísérlet $reconnect_duration_in_seconds$ másodperc múlva", "submitDebugLog": "Fejlesztői naplófájl", "debugLog": "Fejlesztői napló", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Release notes megnyitása", "goToSupportPage": "Támogatás megnyitása", "menuReportIssue": "Hiba bejelentése", @@ -109,13 +110,14 @@ "continue": "Tovább", "error": "Hiba", "delete": "Törlés", - "deletePublicWarning": "Biztosan ezt akarja tenni? Ez mindenki számára véglegesen törli ezt az üzenetet ebből a nyílvános csoportból.", - "deleteMultiplePublicWarning": "Biztosan ezt akarja tenni? Ez mindenki számára véglegesen törli ezeket az üzeneteket ebből a nyílvános csoportból.", - "deleteWarning": "Biztos vagy benne? A 'Törlés'-re kattintással végleg eltávolítod az üzenetet erről az eszközről.", - "deleteMultipleWarning": "Biztosan ezt akarja tenni? A 'Törlés' gombra kattintva végleg törli az üzeneteket erről az eszközről.", "messageDeletionForbidden": "Nincs joga mások üzeneteit törölni", - "deleteThisMessage": "Üzenet törlése", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Üzenetek törlése", "deleted": "Törölve", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Feladó", "to": "címzett", "sent": "Elküldve", @@ -124,14 +126,6 @@ "groupMembers": "Csoport tagjai", "moreInformation": "További információ", "resend": "Újraküldés", - "deleteMessage": "Üzenet törlése", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Üzenetek törlése", - "deleteMessageForEveryone": "Törölni az üzenetet mindenki számára", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Törölni az üzeneteket mindenki számára", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Véglegesen törlöd ezt a beszélgetést?", "clearAllData": "Az összes adat törlése", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Az ön Session azonosítójának kimásolása", "copyOpenGroupURL": "Csoport URL-jének másolása", "save": "Mentés", + "saveLogToDesktop": "Save log to desktop", "saved": "Elmentve", "permissions": "Jogosultságok", "general": "Általános", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Hivatkozások előnézeti képének küldése", "linkPreviewDescription": "Az előnézetek a legtöbb linknél támogatottak", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofon és kamera", - "mediaPermissionsDescription": "Hozzáférés a kamerához és a mikrofonhoz", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Helyesírás ellenőrzése", "spellCheckDescription": "Az üzenetíró dobozba gépelt szöveg helyesírás-ellenőrzésének engedélyezése", "spellCheckDirty": "Újra kell indítanod a Session-t a beállítások érvényesítéséhez", @@ -421,6 +416,7 @@ "unpinConversation": "Feltűzött beszélgetés eltávolítása", "pinConversationLimitTitle": "Feltűzött beszélgetések limite", "pinConversationLimitToastDescription": "Csak $number$ beszélgetést tudsz feltűzni", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "A legkorábbi olvasatlan üzenet feljebb található", "sendRecoveryPhraseTitle": "Helyreállító kódmondat küldése", "sendRecoveryPhraseMessage": "A helyreállító kódmondatod készülsz elküldeni, amellyel hozzá lehet férni fiókodhoz. Biztosan el akarod küldeni?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Biztosítsd fiókod a helyreállítási kulcsod elmentésével. Fedd fel a kulcsod, majd biztonságosan tedd el.", "recoveryPhraseRevealButtonText": "Helyreállító kódmondat felfedése", "notificationSubtitle": "Értesítések - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/id/messages.json b/_locales/id/messages.json index 6c9918103..b01c9f146 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Mencoba menyambung kembali dalam $reconnect_duration_in_seconds$ detik", "submitDebugLog": "Catatan Awakutu", "debugLog": "Catatan Awakutu", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Lihat Catatan Rilis", "goToSupportPage": "Cek halaman bantuan", "menuReportIssue": "Laporkan masalah ", @@ -109,13 +110,14 @@ "continue": "Lanjutkan", "error": "Galat", "delete": "Hapus", - "deletePublicWarning": "Anda yakin? Aksi ini akan menghapus pesan ini secara permanen untuk seluruh orang di grup terbuka ini.", - "deleteMultiplePublicWarning": "Anda yakin? Aksi ini akan menghapus pesan-pesan ini secara permanen untuk seluruh orang di grup terbuka ini.", - "deleteWarning": "Apakah Anda yakin? Menekan \"hapus\" akan menghapus pesan secara permanen hanya pada perangkat ini.", - "deleteMultipleWarning": "Anda yakin? Aksi ini akan menghapus pesan secara permanen hanya di perangkat ini.", "messageDeletionForbidden": "Anda tidak diizinkan untuk menghapus pesan lainnya", - "deleteThisMessage": "Hapus pesan", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Hapus pesan", "deleted": "Terhapus", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Dari:", "to": "Kepada:", "sent": "Terkirim", @@ -124,14 +126,6 @@ "groupMembers": "Anggota grup", "moreInformation": "Informasi lebih lanjut", "resend": "Kirim ulang", - "deleteMessage": "Hapus pesan", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Hapus pesan", - "deleteMessageForEveryone": "Hapus pesan untuk semua", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Hapus pesan untuk semua", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Hapus obrolan ini selamanya?", "clearAllData": "Hapus semua data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Salin Session ID", "copyOpenGroupURL": "Salin URL grup", "save": "Simpan", + "saveLogToDesktop": "Save log to desktop", "saved": "Tersimpan", "permissions": "Perizinan", "general": "Umum", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Kirim pratinjau tautan", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofon dan kamera", - "mediaPermissionsDescription": "Izinkan akses ke kamera dan mikrofon", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Pemeriksa Ejaan", "spellCheckDescription": "Aktifkan pengecek ejaan dalam kotak pesan", "spellCheckDirty": "Anda harus muat ulang Session untuk terima pengaturan baru anda", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 9f6eff22a..0572736df 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Tentativo di riconessione tra $reconnect_duration_in_seconds$ secondi.", "submitDebugLog": "Log di debug", "debugLog": "Log di debug", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Vai alle note di rilascio", "goToSupportPage": "Vai alla pagina di supporto", "menuReportIssue": "Segnala un problema", @@ -109,13 +110,14 @@ "continue": "Continua", "error": "Errore", "delete": "Cancella", - "deletePublicWarning": "Sei sicuro? Questo rimuoverà permanentemente questo messaggio a tutti in questo gruppo aperto.", - "deleteMultiplePublicWarning": "Sei sicuro? Questo rimuoverà permanentemente questi messaggi a tutti in questo gruppo aperto.", - "deleteWarning": "Sicuro? Cliccando 'cancella' rimuoverai definitivamente questo messaggio solo da questo dispositivo.", - "deleteMultipleWarning": "Sei sicuro? Fare clic su 'eliminare' rimuoverà permanentemente questi messaggi solo da questo dispositivo.", "messageDeletionForbidden": "Non hai il permesso di eliminare i messaggi degli altri", - "deleteThisMessage": "Cancella messaggio", + "deleteJustForMe": "Elimina solo per me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Elimina messaggi", "deleted": "Eliminata", + "messageDeletedPlaceholder": "Questo messaggio è stato eliminato", "from": "Da:", "to": "A:", "sent": "Inviato", @@ -124,17 +126,9 @@ "groupMembers": "Membri del gruppo", "moreInformation": "Maggiori informazioni", "resend": "Reinvia", - "deleteMessage": "Cancella Messaggio", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Elimina messaggi", - "deleteMessageForEveryone": "Elimina Messaggio Per Tutti", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Elimina Messaggi Per Tutti", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Rimuovere definitivamente la conversazione?", "clearAllData": "Elimina tutti i dati", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Ciò eliminerà permanentemente i tuoi messaggi e contatti.", "deleteContactConfirmation": "Sei sicuro di voler eliminare questa conversazione?", "quoteThumbnailAlt": "Anteprima dell'immagine dal messaggio citato", "imageAttachmentAlt": "Immagine allegata a un messaggio", @@ -146,6 +140,7 @@ "copySessionID": "Copia Session ID", "copyOpenGroupURL": "Copia L'Url Del Gruppo", "save": "Salva", + "saveLogToDesktop": "Save log to desktop", "saved": "Salvato", "permissions": "Permessi", "general": "Generale", @@ -153,9 +148,9 @@ "savedTheFile": "Media salvati da $name$", "linkPreviewsTitle": "Invia Anteprime Dei Link", "linkPreviewDescription": "Previews are supported for most urls.", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microfono e Fotocamera", - "mediaPermissionsDescription": "Concedi l'accesso alla fotocamera e al microfono", + "linkPreviewsConfirmMessage": "Non avrai la protezione completa dei metadati quando invierai le anteprime dei link.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Controllo ortografico", "spellCheckDescription": "Abilita il controllo ortografico del testo inserito nella casella di composizione", "spellCheckDirty": "È necessario riavviare Session per applicare le modifiche", @@ -421,6 +416,7 @@ "unpinConversation": "Rimuovi conversazione", "pinConversationLimitTitle": "Limite conversazioni fissate", "pinConversationLimitToastDescription": "Puoi fissare solo $number$ conversazioni", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Il primo messaggio non letto è sopra", "sendRecoveryPhraseTitle": "Invio Frase Di Recupero", "sendRecoveryPhraseMessage": "Stai tentando di inviare la frase di recupero che può essere utilizzata per accedere al tuo account. Sei sicuro di voler inviare questo messaggio?", @@ -431,30 +427,40 @@ "dialogClearAllDataDeletionQuestion": "Vuoi formattare solo questo dispositivo o cancellare del tutto l'account?", "deviceOnly": "Solo dispositivo", "entireAccount": "Tutto l'account", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "areYouSureDeleteDeviceOnly": "Sei sicuro di voler cancellare i dati solo sul tuo dispositivo?", + "areYouSureDeleteEntireAccount": "Sei sicuro di voler eliminare l'intero tuo account, inclusi i dati di rete?", + "iAmSure": "Sono sicuro", "recoveryPhraseSecureTitle": "Hai quasi finito!", "recoveryPhraseRevealMessage": "Proteggi il tuo account salvando la tua frase di recupero. Mostra la tua frase di recupero, quindi salvala in modo sicuro per proteggerla.", "recoveryPhraseRevealButtonText": "Mostra Frase di Recupero", "notificationSubtitle": "Notifiche - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Fai il nostro sondaggio di Session", + "goToOurSurvey": "Vai al sondaggio", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Accetta", + "decline": "Rifiuta", + "endCall": "Termina chiamata", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Impossibile avviare una nuova chiamata", + "callMissed": "Chiamata persa da $name$", + "callMissedTitle": "Chiamata persa", + "noCameraFound": "Nessuna fotocamera è stata trovata", + "noAudioInputFound": "Nessun ingresso audio trovato", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 2937e0baa..530fb78c7 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "$reconnect_duration_in_seconds$ 秒後に再接続を行います。", "submitDebugLog": "デバッグログ", "debugLog": "デバッグログ", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "リリースノートを閲覧", "goToSupportPage": "サポートページへ", "menuReportIssue": "バグを報告", @@ -109,13 +110,14 @@ "continue": "続行", "error": "エラー", "delete": "削除", - "deletePublicWarning": "削除」を選択したら公開グループからメッセージが永久削除されます。よろしいですか?", - "deleteMultiplePublicWarning": "削除」を選択したら公開グループからメッセージが永久削除されます。よろしいですか?", - "deleteWarning": "「削除」を選択したら自分の端末からのみメッセージが永久削除されます。よろしいですか?", - "deleteMultipleWarning": "「削除」を選択したら自分の端末からのみメッセージが永久削除されます。よろしいですか?", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "メッセージの削除", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "メッセージを削除", "deleted": "メッセージが削除されました", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "送信", @@ -124,14 +126,6 @@ "groupMembers": "グループメンバー", "moreInformation": "詳細", "resend": "再送", - "deleteMessage": "メッセージの削除", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "メッセージを削除", - "deleteMessageForEveryone": "全員からメッセージを削除", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "全員からメッセージを削除", - "deleteForEveryone": "全員から削除", "deleteConversationConfirmation": "この会話を完全に消去しますが,よろしいですか?", "clearAllData": "すべてのデータを消去する", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Session ID をコピー", "copyOpenGroupURL": "Copy Group's URL", "save": "保存", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "アクセス許可", "general": "一般", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "リンクプレビューを送る", "linkPreviewDescription": "プレビューはImgur、Instagram、Pinterest、RedditおよびYouTubeリンクをサポートしています", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "マイクとカメラ", - "mediaPermissionsDescription": "カメラとマイクへのアクセスを許可する", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "スペルチェック", "spellCheckDescription": "メッセージボックスに入力されたテキストのスペルチェックを有効にする", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ka/messages.json b/_locales/ka/messages.json index 2ffb3e264..192eaff3b 100644 --- a/_locales/ka/messages.json +++ b/_locales/ka/messages.json @@ -50,16 +50,16 @@ "submit": "გაგზავნა", "markAllAsRead": "ყველას მონიშვნა წაკითხულად", "incomingError": "Error handling incoming message", - "media": "Media", - "mediaEmptyState": "No media", - "documents": "Documents", - "documentsEmptyState": "No documents", - "today": "Today", - "yesterday": "Yesterday", - "thisWeek": "This week", - "thisMonth": "This Month", - "voiceMessage": "Voice Message", - "dangerousFileType": "For security reasons, this file type cannot be sent", + "media": "მედია", + "mediaEmptyState": "მედია ცარიელია", + "documents": "დოკუმენტები", + "documentsEmptyState": "დოკუმენტები არ არის", + "today": "დღეს", + "yesterday": "გუშინ", + "thisWeek": "ამ კვირაში", + "thisMonth": "ამ თვეს", + "voiceMessage": "ხმოვანი შეტყობინება", + "dangerousFileType": "უსაფრთხოების მიზნით, ამ ტიპის ფაილის გაგზავნა შეუძლებელია", "stagedPreviewThumbnail": "Draft thumbnail link preview for $domain$", "previewThumbnail": "Thumbnail link preview for $domain$", "stagedImageAttachment": "Draft image attachment: $path$", @@ -68,37 +68,38 @@ "maximumAttachments": "Maximum number of attachments reached. Please send remaining attachments in a separate message.", "fileSizeWarning": "Attachment exceeds size limits for the type of message you're sending.", "unableToLoadAttachment": "Sorry, there was an error setting your attachment.", - "offline": "Offline", + "offline": "ხაზგარეშე", "checkNetworkConnection": "Check your network connection.", "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", - "about": "About", + "about": "შესახებ", "speech": "Speech", - "show": "Show", + "show": "ჩვენება", "sessionMessenger": "Session", - "search": "Search", - "noSearchResults": "No results found for \"$searchTerm$\"", - "conversationsHeader": "Conversations", - "contactsHeader": "Contacts", - "messagesHeader": "Messages", - "settingsHeader": "Settings", + "search": "ძიება", + "noSearchResults": "\"$searchTerm$\" -ზე არაფერი მოიძებნა", + "conversationsHeader": "საუბრები", + "contactsHeader": "კონტაქტები", + "messagesHeader": "შეტყობინებები", + "settingsHeader": "პარამეტრები", "typingAlt": "Typing animation for this conversation", "contactAvatarAlt": "Avatar for contact $name$", - "downloadAttachment": "Download Attachment", - "replyToMessage": "Reply to message", - "replyingToMessage": "Replying to:", + "downloadAttachment": "მიმაგრებული ფაილის გადმოტვირთვა", + "replyToMessage": "პასუხი", + "replyingToMessage": "პასუხობთ:", "originalMessageNotFound": "Original message not found", "originalMessageNotAvailable": "Original message no longer available", "messageFoundButNotLoaded": "Original message found, but not loaded. Scroll up to load it.", - "recording": "Recording", - "you": "You", + "recording": "ჩანაწერი", + "you": "თქვენ", "audioPermissionNeededTitle": "Microphone Access Required", "audioPermissionNeeded": "You can enable microphone access under: Settings (Gear icon) => Privacy", - "audio": "Audio", + "audio": "აუდიო", "video": "ვიდეო", "photo": "ფოტო", "cannotUpdate": "ვერ განახლდა", @@ -109,13 +110,14 @@ "continue": "გაგრძელება", "error": "შეცდომა", "delete": "წაშლა", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Sent", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete for Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Save", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/km/messages.json b/_locales/km/messages.json index c9aa95a7f..aea5dd026 100644 --- a/_locales/km/messages.json +++ b/_locales/km/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "កំពុងព្យាយាមតភ្ជាប់ឡើងវិញក្នុងពេល $reconnect_duration_in_seconds$ វិនាទី", "submitDebugLog": "កំណត់ត្រាបញ្ហា", "debugLog": "កំណត់ត្រាបញ្ហា", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "ចូលទៅកាន់កំណត់ចេញថ្មី", "goToSupportPage": "ចូលទៅកាន់ទំព័រគាំទ្រ", "menuReportIssue": "រាយការណ៍បញ្ហាមួយ", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "បញ្ហា", "delete": "លុប", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "តើអ្នកច្បាស់ទេ? ចុច 'លុប' នឹងសារនេះ ចេញពីឧបករណ៍នេះរហូត។", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "លុបសារនេះ", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "លុបសារ", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "ពី", "to": "ទៅ", "sent": "បានផ្ញើ", @@ -124,14 +126,6 @@ "groupMembers": "សមាជិកក្រុម", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "លុបសារ", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "លុបសារ", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "លុបការសន្ទនានេះចោលរហូត?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "រក្សាទុក", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "ការអនុញ្ញាត", "general": "ទូទៅ", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "បើកសិទ្ធិប្រើប្រាស់កាមេរ៉ា និងម៉ៃក្រូហ្វូន", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "បើកការត្រួតពិនិត្យអក្ខរាវិរុទ្ធនៃពាក្យដែលបានបញ្ចូលក្នុងប្រអប់ផ្ញើសារ", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/kn/messages.json b/_locales/kn/messages.json index fa57f7c4c..8428cbf5a 100644 --- a/_locales/kn/messages.json +++ b/_locales/kn/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Error", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Sent", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Save", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 45eef5046..bfab66160 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "계속", "error": "에러", "delete": "지우기", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "경고 지우기", - "deleteMultipleWarning": "여러 개의 경고를 지우기", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete this message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From", "to": "to", "sent": "전송됨", @@ -124,14 +126,6 @@ "groupMembers": "그룹 구성원", "moreInformation": "더 많은 정보", "resend": "재전송", - "deleteMessage": "메시지 지우기", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete messages", - "deleteMessageForEveryone": "모두에게서 메시지 지우기", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "저장", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "일반 설정", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/lt/messages.json b/_locales/lt/messages.json index 6e7474a14..fc59dc7ed 100644 --- a/_locales/lt/messages.json +++ b/_locales/lt/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Bandoma jungtis iš naujo po $reconnect_duration_in_seconds$ sekundžių", "submitDebugLog": "Derinimo žurnalas", "debugLog": "Derinimo žurnalas", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Pereiti į laidos informaciją", "goToSupportPage": "Pereiti į palaikymo puslapį", "menuReportIssue": "Pranešti apie klaidą", @@ -109,13 +110,14 @@ "continue": "Tęsti", "error": "Klaida", "delete": "Ištrinti", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Ar tikrai? Spustelėjus \"Ištrinti\", ši žinutė visiems laikams bus pašalinta tik iš šio įrenginio.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Ištrinti šią žinutę", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Ištrinti žinutes", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Nuo", "to": "skirta", "sent": "Išsiųsta", @@ -124,14 +126,6 @@ "groupMembers": "Grupės dalyviai", "moreInformation": "Daugiau informacijos", "resend": "Siųsti iš naujo", - "deleteMessage": "Ištrinti žinutę", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Ištrinti žinutes", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Ar ištrinti šį pokalbį visiems laikams?", "clearAllData": "Išvalyti visus duomenis", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Kopijuoti Session ID", "copyOpenGroupURL": "Kopijuoti grupės URL", "save": "Įrašyti", + "saveLogToDesktop": "Save log to desktop", "saved": "Įrašyta", "permissions": "Leidimai", "general": "Bendra", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Siųsti nuorodų peržiūras", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofonas ir kamera", - "mediaPermissionsDescription": "Leisti prieigą prie kameros ir mikrofono", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Rašybos tikrinimas", "spellCheckDescription": "Įjungti rašybos tikrinimą tekstui, kuris įvedamas į žinutės rašymo langelį", "spellCheckDirty": "Norėdami taikyti naujus nustatymus, privalote paleisti Session iš naujo", @@ -421,6 +416,7 @@ "unpinConversation": "Atsegti pokalbį", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Dalyvaukite mūsų seanso apklausoje ", + "goToOurSurvey": "Eikite į mūsų apklausą", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Priimti", + "decline": "Atmesti", + "endCall": "Baigti pokalbį", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Neįmanoma pradėti naujo skambučio", + "callMissed": "Praleistas skambutis nuo $name$", + "callMissedTitle": "Praleistas skambutis", + "noCameraFound": "Kameros nerasta", + "noAudioInputFound": "Nerasta garso įvesties", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/mk/messages.json b/_locales/mk/messages.json index b1c199b60..296dcd434 100644 --- a/_locales/mk/messages.json +++ b/_locales/mk/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Грешка", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete this message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Избриши пораки", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From", "to": "to", "sent": "Испратено", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Избриши пораки", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Зачувај", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "Општо", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/nb/messages.json b/_locales/nb/messages.json index fa57f7c4c..8428cbf5a 100644 --- a/_locales/nb/messages.json +++ b/_locales/nb/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Error", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Sent", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Save", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 6d092e762..484706404 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -1,5 +1,5 @@ { - "privacyPolicy": "Terms & Privacy Policy", + "privacyPolicy": "privacybeleid", "copyErrorAndQuit": "Foutmelding kopiëren en afsluiten", "unknown": "Onbekend", "databaseError": "Databasefout", @@ -29,14 +29,14 @@ "viewMenuZoomOut": "Uitzoomen", "viewMenuToggleFullScreen": "Volledigschermmodus", "viewMenuToggleDevTools": "Ontwikkelopties weergeven", - "contextMenuNoSuggestions": "No Suggestions", + "contextMenuNoSuggestions": "Geen suggesties ", "openGroupInvitation": "Open group uitnodiging", - "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", - "enterSessionIDOrONSName": "Enter Session ID or ONS name", + "joinOpenGroupAfterInvitationConfirmationTitle": "groepnaam\n", + "joinOpenGroupAfterInvitationConfirmationDesc": "Weet je zeker dat je lid wilt worden van de $roomName$ open groep?", + "enterSessionIDOrONSName": "Voer Sessie ID of ONS naam in", "loading": "Bezig met laden…", "optimizingApplication": "Toepassing aan het optimaliseren…", - "done": "Done", + "done": "Gereed", "me": "Ik", "view": "Bekijken", "youLeftTheGroup": "Je hebt de groep verlaten", @@ -73,6 +73,7 @@ "attemptingReconnection": "Poging tot opnieuw verbinden over $reconnect_duration_in_seconds$ seconden", "submitDebugLog": "Foutopsporingslogboek", "debugLog": "Foutopsporingslogboek", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Ga naar uitgaveopmerkingen", "goToSupportPage": "Ga naar ondersteuningspagina", "menuReportIssue": "Meld een probleem", @@ -101,70 +102,64 @@ "audio": "Geluid", "video": "Video", "photo": "Foto", - "cannotUpdate": "Cannot Update", - "cannotUpdateDetail": "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", + "cannotUpdate": "Kan niet updaten", + "cannotUpdateDetail": "Sessie Desktop kon niet worden bijgewerkt, maar er is een nieuwe versie beschikbaar. Ga naar https://getsessie. rg/ en installeer de nieuwe versie handmatig, neem dan contact op met support of maak een bug over dit probleem.", "ok": "Begrepen", "cancel": "Annuleren", - "close": "Close", - "continue": "Continue", + "close": "Sluiten", + "continue": "Doorgaan", "error": "Fout", "delete": "Wissen", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Weet je het zeker? Door op ‘Wissen’ te klikken, wordt dit bericht voorgoed van enkel dit apparaat gewist.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", - "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Dit bericht wissen", - "deleted": "Deleted", + "messageDeletionForbidden": "Je hebt geen toestemming om andermans berichten te verwijderen", + "deleteJustForMe": "Verwijder alleen voor mij", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Berichten wissen", + "deleted": "Verwijderd", + "messageDeletedPlaceholder": "Dit bericht is verwijderd", "from": "Van", "to": "aan", "sent": "Verzonden", "received": "Ontvangen", "sendMessage": "Verzend een bericht", "groupMembers": "Groepsleden", - "moreInformation": "More information", - "resend": "Resend", - "deleteMessage": "Bericht wissen", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Berichten wissen", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", + "moreInformation": "Meer informatie", + "resend": "Opnieuw verzenden", "deleteConversationConfirmation": "Dit gesprek voorgoed wissen?", - "clearAllData": "Clear All Data", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", - "deleteContactConfirmation": "Are you sure you want to delete this conversation?", + "clearAllData": "Wis alle gegevens", + "deleteAccountWarning": "Hiermee worden uw berichten en contactpersonen permanent verwijderd.", + "deleteContactConfirmation": "Weet je zeker dat je deze conversatie wilt verwijderen?", "quoteThumbnailAlt": "Miniatuur van afbeelding uit aangehaald bericht", "imageAttachmentAlt": "Afbeelding toegevoegd aan bericht", "videoAttachmentAlt": "Schermafdruk van video toegevoegd aan bericht", "lightboxImageAlt": "Afbeelding verstuurd in gesprek", "imageCaptionIconAlt": "Pictogram dat laat zien dat deze afbeelding een bijschrift heeft", "addACaption": "Voeg een bijschrift toe…", - "copy": "Copy", - "copySessionID": "Copy Session ID", - "copyOpenGroupURL": "Copy Group's URL", + "copy": "Kopiëren", + "copySessionID": "Sessie-ID kopiëren", + "copyOpenGroupURL": "Kopiëer groep URL", "save": "Opslaan", - "saved": "Saved", + "saveLogToDesktop": "Save log to desktop", + "saved": "Opgeslagen", "permissions": "Toestemmingen", "general": "Algemeen", - "tookAScreenshot": "$name$ took a screenshot", - "savedTheFile": "Media saved by $name$", - "linkPreviewsTitle": "Send Link Previews", - "linkPreviewDescription": "Previews are supported for most urls", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Geef toestemming om de camera en microfoon te gebruiken", - "spellCheckTitle": "Spell Check", + "tookAScreenshot": "$name$ heeft een schermafdruk gemaakt", + "savedTheFile": "Media opgeslagen door $name$", + "linkPreviewsTitle": "Verstuur Link-voorbeelden", + "linkPreviewDescription": "Voorvertoningen worden ondersteund voor de meeste urls", + "linkPreviewsConfirmMessage": "Je hebt geen volledige bescherming van metadata bij het verzenden van link-voorbeelden.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", + "spellCheckTitle": "Spellingcontrole", "spellCheckDescription": "Gebruik spellingscontrole voor de tekst in het berichtinvoerveld", - "spellCheckDirty": "You must restart Session to apply your new settings", + "spellCheckDirty": "Je moet Session opnieuw starten, om uw nieuwe instellingen toe te passen", "notifications": "Meldingen", - "readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).", - "readReceiptSettingTitle": "Read Receipts", - "typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).", - "typingIndicatorsSettingTitle": "Typing Indicators", - "zoomFactorSettingTitle": "Zoom Factor", + "readReceiptSettingDescription": "Bekijk en deel wanneer berichten zijn gelezen (laat een lees bevestiging toe in alle sessies).", + "readReceiptSettingTitle": "Leesbevestigingen", + "typingIndicatorsSettingDescription": "Bekijk en deel wanneer berichten worden getypt (geldt voor alle sessies).", + "typingIndicatorsSettingTitle": "Typindicatoren", + "zoomFactorSettingTitle": "Zoom factor", "notificationSettingsDialog": "Toon wanneer een bericht ontvangen wordt meldingen die het volgende weergeven:", "disableNotifications": "Meldingen uitschakelen", "nameAndMessage": "Naam van afzender en berichtinhoud", @@ -183,7 +178,7 @@ "timestamp_m": "1 minuut", "timestamp_h": "1 uur", "timestampFormat_M": "D MMM", - "messageBodyMissing": "Please enter a message body.", + "messageBodyMissing": "Voeg a. u. b. een bericht toe.", "unblockToSend": "Deblokkeer dit contact om een bericht te verzenden.", "unblockGroupToSend": "Deblokkeer deze groep om een bericht te verzenden.", "youChangedTheTimer": "Je hebt de timer voor zelf-wissende berichten op $time$gezet", @@ -202,14 +197,14 @@ "timerOption_1_day": "1 dag", "timerOption_1_week": "1 week", "disappearingMessages": "Zelf-wissende berichten", - "changeNickname": "Change Nickname", + "changeNickname": "Verander bijnaam", "clearNickname": "Clear nickname", - "nicknamePlaceholder": "New Nickname", - "changeNicknameMessage": "Enter a nickname for this user", + "nicknamePlaceholder": "Nieuwe bijnaam", + "changeNicknameMessage": "Vul een bijnaam in voor deze gebruiker", "timerOption_0_seconds_abbreviated": "uit", - "timerOption_5_seconds_abbreviated": "5s", - "timerOption_10_seconds_abbreviated": "10s", - "timerOption_30_seconds_abbreviated": "30s", + "timerOption_5_seconds_abbreviated": "5 seconden", + "timerOption_10_seconds_abbreviated": "10 seconden", + "timerOption_30_seconds_abbreviated": "30 seconden", "timerOption_1_minute_abbreviated": "1m", "timerOption_5_minutes_abbreviated": "5m", "timerOption_30_minutes_abbreviated": "30m", @@ -223,238 +218,249 @@ "youDisabledDisappearingMessages": "Je hebt zelf-wissende berichten uitgeschakeld", "timerSetTo": "Timer ingesteld op $time$", "noteToSelf": "Notitie aan mezelf", - "hideMenuBarTitle": "Hide Menu Bar", - "hideMenuBarDescription": "Toggle system menu bar visibility", + "hideMenuBarTitle": "Menubalk verbergen", + "hideMenuBarDescription": "Zichtbaarheid systeemmenu in-/uitschakelen", "startConversation": "Begin een nieuw gesprek…", "invalidNumberError": "Ongeldig nummer", - "failedResolveOns": "Failed to resolve ONS name", - "successUnlinked": "Your device was unlinked successfully", - "autoUpdateSettingTitle": "Auto Update", + "failedResolveOns": "Gefaald de ONS naam op te lossen", + "successUnlinked": "Je apparaat is met succes ontkoppeld", + "autoUpdateSettingTitle": "Automatisch bijwerken", "autoUpdateSettingDescription": "Automatically check for updates on launch", "autoUpdateNewVersionTitle": "Update voor Session beschikbaar", "autoUpdateNewVersionMessage": "Er is een nieuwe versie van Session beschikbaar.", "autoUpdateNewVersionInstructions": "Klik op Session herstarten’ om de updates toe te passen.", "autoUpdateRestartButtonLabel": "Session herstarten", "autoUpdateLaterButtonLabel": "Later", - "autoUpdateDownloadButtonLabel": "Download", + "autoUpdateDownloadButtonLabel": "Downloaden", "autoUpdateDownloadedMessage": "The new update has been downloaded.", - "autoUpdateDownloadInstructions": "Would you like to download the update?", + "autoUpdateDownloadInstructions": "Wil je de update downloaden?", "leftTheGroup": "$name$ heeft de groep verlaten", "multipleLeftTheGroup": "$name$ heeft de groep verlaten", "updatedTheGroup": "De groep is aangepast", "titleIsNow": "De titel is nu “$name$”", "joinedTheGroup": "$name$ is lid geworden van de groep", "multipleJoinedTheGroup": "$names$ is lid geworden van de groep", - "kickedFromTheGroup": "$name$ was removed from the group.", - "multipleKickedFromTheGroup": "$name$ were removed from the group.", - "blockUser": "Block", - "unblockUser": "Unblock", - "unblocked": "Unblocked", - "blocked": "Blocked", - "blockedSettingsTitle": "Blocked contacts", - "unbanUser": "Unban User", + "kickedFromTheGroup": "$name$ is verwijderd uit de groep.", + "multipleKickedFromTheGroup": "$name$ is verwijderd uit de groep.", + "blockUser": "Blokkeren", + "unblockUser": "Blokkering opheffen", + "unblocked": "Gedeblokkeerd", + "blocked": "Geblokkeerd", + "blockedSettingsTitle": "Geblokkeerde contacten", + "unbanUser": "Gebruiker deblokkeren", "unbanUserConfirm": "Are you sure you want to unban user?", - "userUnbanned": "User unbanned successfully", - "userUnbanFailed": "Unban failed!", - "banUser": "Ban User", + "userUnbanned": "Gebruiker gedeblokkeerd", + "userUnbanFailed": "Deblokkeren mislukt!", + "banUser": "Gebruiker uitsluiten", "banUserConfirm": "Are you sure you want to ban user?", - "banUserAndDeleteAll": "Ban and Delete All", - "banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?", + "banUserAndDeleteAll": "Blokkeer en verwijder alles", + "banUserAndDeleteAllConfirm": "Weet je zeker dat je de gebruiker wilt verbieden en al zijn berichten wilt verwijderen?", "userBanned": "User banned successfully", - "userBanFailed": "Ban failed!", - "leaveGroup": "Leave Group", - "leaveAndRemoveForEveryone": "Leave Group and remove for everyone", - "leaveGroupConfirmation": "Are you sure you want to leave this group?", - "leaveGroupConfirmationAdmin": "As you are the admin of this group, if you leave it it will be removed for every current members. Are you sure you want to leave this group?", - "cannotRemoveCreatorFromGroup": "Cannot remove this user", - "cannotRemoveCreatorFromGroupDesc": "You cannot remove this user as they are the creator of the group.", - "noContactsForGroup": "You don't have any contacts yet", - "failedToAddAsModerator": "Failed to add user as moderator", - "failedToRemoveFromModerator": "Failed to remove user from the moderator list", - "copyMessage": "Copy message text", - "selectMessage": "Select message", - "editGroup": "Edit group", - "editGroupName": "Edit group name", - "updateGroupDialogTitle": "Updating $name$...", - "showRecoveryPhrase": "Recovery Phrase", - "yourSessionID": "Your Session ID", - "setAccountPasswordTitle": "Set Account Password", - "setAccountPasswordDescription": "Require password to unlock Session’s screen. You can still receive message notifications while Screen Lock is enabled. Session’s notification settings allow you to customize information that is displayed", - "changeAccountPasswordTitle": "Change Account Password", - "changeAccountPasswordDescription": "Change your password", - "removeAccountPasswordTitle": "Remove Account Password", - "removeAccountPasswordDescription": "Remove the password associated with your account", - "enterPassword": "Please enter your password", - "confirmPassword": "Confirm password", - "pasteLongPasswordToastTitle": "The clipboard content exceeds the maximum password length of $max_pwd_len$ characters.", - "showRecoveryPhrasePasswordRequest": "Please enter your password", - "recoveryPhraseSavePromptMain": "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don't give it to anyone.", - "invalidOpenGroupUrl": "Invalid URL", - "copiedToClipboard": "Copied to clipboard", - "passwordViewTitle": "Type In Your Password", - "unlock": "Unlock", - "password": "Password", - "setPassword": "Set Password", - "changePassword": "Change Password", - "removePassword": "Remove Password", - "maxPasswordAttempts": "Invalid Password. Would you like to reset the database?", - "typeInOldPassword": "Please type in your old password", - "invalidOldPassword": "Old password is invalid", - "invalidPassword": "Invalid password", - "noGivenPassword": "Please enter your password", - "passwordsDoNotMatch": "Passwords do not match", - "setPasswordInvalid": "Passwords do not match", - "changePasswordInvalid": "The old password you entered is incorrect", - "removePasswordInvalid": "Incorrect password", - "setPasswordTitle": "Set Password", - "changePasswordTitle": "Changed Password", - "removePasswordTitle": "Removed Password", - "setPasswordToastDescription": "Your password has been set. Please keep it safe.", - "changePasswordToastDescription": "Your password has been changed. Please keep it safe.", - "removePasswordToastDescription": "You have removed your password.", - "publicChatExists": "You are already connected to this open group", - "connectToServerFail": "Couldn't join group", - "connectingToServer": "Connecting...", - "connectToServerSuccess": "Successfully connected to open group", - "setPasswordFail": "Failed to set password", - "passwordLengthError": "Password must be between 6 and 64 characters long", - "passwordTypeError": "Password must be a string", - "passwordCharacterError": "Password must only contain letters, numbers and symbols", - "remove": "Remove", - "invalidSessionId": "Invalid Session ID", - "invalidPubkeyFormat": "Invalid Pubkey Format", - "emptyGroupNameError": "Please enter a group name", - "editProfileModalTitle": "Profile", - "groupNamePlaceholder": "Group Name", - "inviteContacts": "Invite Contacts", - "addModerators": "Add Moderators", - "removeModerators": "Remove Moderators", + "userBanFailed": "Blokkeren mislukt!", + "leaveGroup": "Verlaat groep", + "leaveAndRemoveForEveryone": "Groep verlaten en voor iedereen verwijderen", + "leaveGroupConfirmation": "Weet je zeker dat je deze groep wilt verlaten?", + "leaveGroupConfirmationAdmin": "Je bent de beheerder van deze groep, als je deze verlaat, wordt deze voor alle huidige leden verwijderd. Weet je zeker dat je deze groep wilt verlaten?", + "cannotRemoveCreatorFromGroup": "Kan deze gebruiker niet verwijderen", + "cannotRemoveCreatorFromGroupDesc": "Je kunt deze gebruiker niet verwijderen omdat hij/zij de maker van de groep is.", + "noContactsForGroup": "Je hebt nog geen contacten", + "failedToAddAsModerator": "Gebruiker toevoegen als moderator is mislukt", + "failedToRemoveFromModerator": "Gebruiker verwijderen uit de moderator lijst is mislukt", + "copyMessage": "Kopieer tekst van bericht", + "selectMessage": "Selecteer een bericht", + "editGroup": "Groep bewerken", + "editGroupName": "Groepsnaam bewerken", + "updateGroupDialogTitle": "$name$ bijwerken...", + "showRecoveryPhrase": "Herstel zin", + "yourSessionID": "Je Session ID", + "setAccountPasswordTitle": "Account wachtwoord instellen", + "setAccountPasswordDescription": "Vereis wachtwoord om het Session scherm te ontgrendelen. Je kunt nog steeds berichten ontvangen terwijl schermvergrendeling is ingeschakeld. De meldingsinstellingen van Session geven u de mogelijkheid om informatie die wordt weergegeven aan te passen", + "changeAccountPasswordTitle": "Veranderd account wachtwoord", + "changeAccountPasswordDescription": "Wijzig je wachtwoord", + "removeAccountPasswordTitle": "Verwijder Account Wachtwoord", + "removeAccountPasswordDescription": "Verwijder het wachtwoord dat aan je account is gekoppeld", + "enterPassword": "Voer a. u. b. je wachtwoord in", + "confirmPassword": "Bevestig Wachtwoord", + "pasteLongPasswordToastTitle": "De inhoud van het klembord is langer dan de maximale wachtwoordlengte van $max_pwd_len$ tekens.", + "showRecoveryPhrasePasswordRequest": "Voer a. u. b. je wachtwoord in", + "recoveryPhraseSavePromptMain": "Jou herstel zin is de hoofdsleutel van jou Session ID - Je kunt deze gebruiken om je Session ID te herstellen als je de toegang tot jouw apparaat verliest. Sla jouw herstel zin veilig op en geef het aan niemand.", + "invalidOpenGroupUrl": "Ongeldige URL", + "copiedToClipboard": "Gekopiëerd naar klembord", + "passwordViewTitle": "Typ je wachtwoord", + "unlock": "Ontgrendelen", + "password": "Wachtwoord", + "setPassword": "Instellen wachtwoord", + "changePassword": "Wachtwoord wijzigen", + "removePassword": "Verwijderd wachtwoord", + "maxPasswordAttempts": "Ongeldig wachtwoord. Wilt u de database resetten?", + "typeInOldPassword": "Voer a. u. b. je oude wachtwoord in", + "invalidOldPassword": "Oude wachtwoord is ongeldig", + "invalidPassword": "Ongeldig wachtwoord", + "noGivenPassword": "Voer a. u. b. je wachtwoord in", + "passwordsDoNotMatch": "Wachtwoorden zijn niet hetzelfde", + "setPasswordInvalid": "Wachtwoorden zijn niet hetzelfde", + "changePasswordInvalid": "Het ingevoerde oude wachtwoord is onjuist", + "removePasswordInvalid": "Onjuist wachtwoord", + "setPasswordTitle": "Instellen wachtwoord", + "changePasswordTitle": "Gewijzigd wachtwoord", + "removePasswordTitle": "Verwijderd wachtwoord", + "setPasswordToastDescription": "Je wachtwoord is ingesteld. Houd het veilig.", + "changePasswordToastDescription": "Uw wachtwoord is gewijzigd. Hou het veilig.", + "removePasswordToastDescription": "Je hebt je wachtwoord verwijderd.", + "publicChatExists": "Je bent al verbonden met deze open groep", + "connectToServerFail": "Kon niet toetreden tot groep", + "connectingToServer": "Verbinding maken...", + "connectToServerSuccess": "Succesvol verbonden om een groep te openen", + "setPasswordFail": "Wachtwoord instellen mislukt", + "passwordLengthError": "Wachtwoord moet tussen de 6 en 64 tekens lang zijn", + "passwordTypeError": "Wachtwoord moet een tekenreeks zijn", + "passwordCharacterError": "Wachtwoord mag alleen letters, cijfers en symbolen bevatten", + "remove": "Verwijderen", + "invalidSessionId": "Ongeldige Session ID", + "invalidPubkeyFormat": "Ongeldig Pubkey formaat", + "emptyGroupNameError": "Vul a. u. b een groepsnaam in", + "editProfileModalTitle": "Profiel", + "groupNamePlaceholder": "Groepsnaam", + "inviteContacts": "Contactpersonen uitnodigen", + "addModerators": "Moderatoren toevoegen", + "removeModerators": "Verwijder moderators", "addAsModerator": "Add As Moderator", - "removeFromModerators": "Remove From Moderators", - "add": "Add", - "addingContacts": "Adding contacts to", - "noContactsToAdd": "No contacts to add", - "noMembersInThisGroup": "No other members in this group", - "noModeratorsToRemove": "no moderators to remove", - "onlyAdminCanRemoveMembers": "You are not the creator", - "onlyAdminCanRemoveMembersDesc": "Only the creator of the group can remove users", + "removeFromModerators": "Verwijder uit Moderators", + "add": "Toevoegen", + "addingContacts": "Contactpersonen toevoegen aan", + "noContactsToAdd": "Geen contacten toe te voegen", + "noMembersInThisGroup": "Geen andere leden in deze groep", + "noModeratorsToRemove": "geen moderators te verwijderen", + "onlyAdminCanRemoveMembers": "Jij bent niet de maker", + "onlyAdminCanRemoveMembersDesc": "Alleen de maker van de groep kan gebruikers verwijderen", "createAccount": "Create Account", - "signIn": "Sign In", - "startInTrayTitle": "Start in Tray", - "startInTrayDescription": "Start Session as a minified app ", - "yourUniqueSessionID": "Say hello to your Session ID", - "allUsersAreRandomly...": "Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design.", - "getStarted": "Get started", - "createSessionID": "Create Session ID", - "recoveryPhrase": "Recovery Phrase", - "enterRecoveryPhrase": "Enter your recovery phrase", - "displayName": "Display Name", - "anonymous": "Anonymous", - "removeResidueMembers": "Clicking ok will also remove those members as they left the group.", - "enterDisplayName": "Enter a display name", - "enterOptionalPassword": "Enter password (optional)", - "continueYourSession": "Continue Your Session", - "linkDevice": "Link Device", - "restoreUsingRecoveryPhrase": "Restore your account", - "or": "or", - "ByUsingThisService...": "By using this service, you agree to our Terms of Service and Privacy Policy", - "beginYourSession": "Begin your Session.", - "welcomeToYourSession": "Welcome to your Session", - "newSession": "New Session", - "searchFor...": "Search for conversations or contacts", - "enterSessionID": "Enter Session ID", - "enterSessionIDOfRecipient": "Enter Session ID or ONS name of recipient", - "usersCanShareTheir...": "Users can share their Session ID by going into their account settings and tapping \"Share Session ID\", or by sharing their QR code.", - "message": "Message", - "appearanceSettingsTitle": "Appearance", - "permissionSettingsTitle": "Permissions", + "signIn": "Aanmelden", + "startInTrayTitle": "Start in taakbalk", + "startInTrayDescription": "Start Session als een miniatuur app ", + "yourUniqueSessionID": "Zeg hallo tegen jou Session-ID", + "allUsersAreRandomly...": "Jou Session-ID is het unieke adres dat mensen kunnen gebruiken om contact met u op te nemen op Session. Ontworpen zonder verbinding met je echte identiteit, is jou Session-ID volledig anoniem en privé.", + "getStarted": "Aan de slag", + "createSessionID": "Session ID aanmaken", + "recoveryPhrase": "Uw herstel zin", + "enterRecoveryPhrase": "Voer uw herstel zin in", + "displayName": "Naam weergeven", + "anonymous": "Anoniem", + "removeResidueMembers": "Als u op OK klikt, worden deze leden ook verwijderd bij het verlaten van de groep.", + "enterDisplayName": "Kies een weergavenaam", + "enterOptionalPassword": "Wachtwoord invoeren (optioneel)", + "continueYourSession": "Ga verder met je sessie", + "linkDevice": "Koppel een apparaat", + "restoreUsingRecoveryPhrase": "Herstel je account", + "or": "of", + "ByUsingThisService...": "Door gebruik te maken van deze service, gaat u akkoord met onze Gebruiksvoorwaarden en Privacy Policy", + "beginYourSession": "Begin je sessie.", + "welcomeToYourSession": "Welkom bij Session", + "newSession": "Nieuwe sessie", + "searchFor...": "Zoeken naar gesprekken of contacten", + "enterSessionID": "Uw Session-ID", + "enterSessionIDOfRecipient": "Voer de Session ID van de ontvanger in", + "usersCanShareTheir...": "Gebruikers kunnen hun Session-ID delen door naar hun accountinstellingen te gaan en op \"Deel Session-ID\" te tikken, of door hun QR-code te delen.", + "message": "Bericht", + "appearanceSettingsTitle": "Weergave", + "permissionSettingsTitle": "Toestemmingen", "privacySettingsTitle": "Privacy", - "notificationsSettingsTitle": "Notifications", - "recoveryPhraseEmpty": "Enter your recovery phrase", + "notificationsSettingsTitle": "Meldingen", + "recoveryPhraseEmpty": "Voer uw herstelzin in", "displayNameEmpty": "Please pick a display name", - "members": "$count$ members", - "joinOpenGroup": "Join Open Group", - "newClosedGroup": "New Closed Group", - "createClosedGroupNamePrompt": "Group Name", - "createClosedGroupPlaceholder": "Enter a group name", - "openGroupURL": "Open Group URL", - "enterAnOpenGroupURL": "Enter an open group URL", - "next": "Next", + "members": "$count$ leden", + "joinOpenGroup": "Deelnemen aan Open groep", + "newClosedGroup": "Nieuwe Besloten Groep", + "createClosedGroupNamePrompt": "Groepsnaam", + "createClosedGroupPlaceholder": "Vul een groepsnaam in", + "openGroupURL": "Open Groep-URL openen", + "enterAnOpenGroupURL": "Voer een open groep URL in", + "next": "Volgende", "description": "Description", - "invalidGroupNameTooShort": "Please enter a group name", - "invalidGroupNameTooLong": "Please enter a shorter group name", - "pickClosedGroupMember": "Please pick at least 1 group member", - "closedGroupMaxSize": "A closed group cannot have more than 100 members", - "noBlockedContacts": "No blocked contacts", - "userAddedToModerators": "User added to moderator list", - "userRemovedFromModerators": "User removed from moderator list", - "orJoinOneOfThese": "Or join one of these...", - "helpUsTranslateSession": "Help us Translate Session", - "translation": "Translation", - "closedGroupInviteFailTitle": "Group Invitation Failed", - "closedGroupInviteFailTitlePlural": "Group Invitations Failed", - "closedGroupInviteFailMessage": "Unable to successfully invite a group member", - "closedGroupInviteFailMessagePlural": "Unable to successfully invite all group members", - "closedGroupInviteOkText": "Retry invitations", - "closedGroupInviteSuccessTitlePlural": "Group Invitations Completed", - "closedGroupInviteSuccessTitle": "Group Invitation Succeeded", - "closedGroupInviteSuccessMessage": "Successfully invited closed group members", - "notificationForConvo": "Notifications", - "notificationForConvo_all": "All", - "notificationForConvo_disabled": "Disabled", - "notificationForConvo_mentions_only": "Mentions only", - "onionPathIndicatorTitle": "Path", - "onionPathIndicatorDescription": "Session hides your IP by bouncing your messages through several Service Nodes in Session's decentralized network. These are the countries your connection is currently being bounced through:", - "unknownCountry": "Unknown Country", - "device": "Device", - "destination": "Destination", - "learnMore": "Learn more", - "linkVisitWarningTitle": "Open this link in your browser?", - "linkVisitWarningMessage": "Are you sure you want to open $url$ in your browser?", - "open": "Open", - "audioMessageAutoplayTitle": "Audio Message Autoplay", - "audioMessageAutoplayDescription": "Automatically play consecutively sent audio messages", - "clickToTrustContact": "Click to download media", - "trustThisContactDialogTitle": "Trust $name$?", - "trustThisContactDialogDescription": "Are you sure you want to download media sent by $name$?", - "pinConversation": "Pin Conversation", - "unpinConversation": "Unpin Conversation", - "pinConversationLimitTitle": "Pinned conversations limit", - "pinConversationLimitToastDescription": "You can only pin $number$ conversations", - "latestUnreadIsAbove": "First unread message is above", - "sendRecoveryPhraseTitle": "Sending Recovery Phrase", - "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", - "dialogClearAllDataDeletionFailedTitle": "Data not deleted", - "dialogClearAllDataDeletionFailedDesc": "Data not deleted with an unknown error. Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedTitleQuestion": "Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", - "dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?", - "deviceOnly": "Device Only", - "entireAccount": "Entire Account", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", - "recoveryPhraseSecureTitle": "You're almost finished!", - "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", - "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", - "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "invalidGroupNameTooShort": "Vul een groepsnaam in", + "invalidGroupNameTooLong": "Vul a. u. b een kortere groepsnaam in", + "pickClosedGroupMember": "Kies ten minste één groepslid", + "closedGroupMaxSize": "Een besloten groep kan niet meer dan 100 leden hebben", + "noBlockedContacts": "Geen geblokkeerde contactpersonen", + "userAddedToModerators": "Gebruiker toegevoegd aan moderator lijst", + "userRemovedFromModerators": "Gebruiker verwijderd uit moderatorlijst", + "orJoinOneOfThese": "Of neem deel aan een van deze...", + "helpUsTranslateSession": "Help ons om Session the vertalen", + "translation": "Vertalingen", + "closedGroupInviteFailTitle": "Uitnodiging voor groep mislukt", + "closedGroupInviteFailTitlePlural": "Groepsuitnodigingen mislukt", + "closedGroupInviteFailMessage": "Kan een groepslid niet uitnodigen", + "closedGroupInviteFailMessagePlural": "Kan een groepslid niet uitnodigen", + "closedGroupInviteOkText": "Uitnodigingen nogmaals proberen", + "closedGroupInviteSuccessTitlePlural": "Groepsuitnodigingen voltooid", + "closedGroupInviteSuccessTitle": "Groepsuitnodiging geslaagd", + "closedGroupInviteSuccessMessage": "Besloten groepsleden met succes uitgenodigd", + "notificationForConvo": "Meldingen", + "notificationForConvo_all": "Alle", + "notificationForConvo_disabled": "Gedeactiveerd", + "notificationForConvo_mentions_only": "Alleen vermeldingen,", + "onionPathIndicatorTitle": "Pad", + "onionPathIndicatorDescription": "Session verbergt uw IP door uw berichten te verzenden via meerdere Service Nodes in het gedecentraliseerde Session netwerk. Uw verbinding wordt momenteel verzonden via deze landen:", + "unknownCountry": "Onbekend land", + "device": "Apparaat", + "destination": "Bestemming", + "learnMore": "Meer informatie", + "linkVisitWarningTitle": "Deze link in je browser openen?", + "linkVisitWarningMessage": "Weet je zeker dat je $url$ in je browser wilt openen?", + "open": "Openen", + "audioMessageAutoplayTitle": "Audio bericht automatisch afspelen", + "audioMessageAutoplayDescription": "Automatisch opeenvolgende verzonden audioberichten afspelen", + "clickToTrustContact": "Klik om media te downloaden", + "trustThisContactDialogTitle": "Vertrouw $name$?", + "trustThisContactDialogDescription": "Weet je zeker dat je media van $name$ wilt downloaden?", + "pinConversation": "Gesprek vastzetten", + "unpinConversation": "Gesprek losmaken", + "pinConversationLimitTitle": "Limiet vastgezette gesprekken", + "pinConversationLimitToastDescription": "Je kan alleen $number$ gesprekken vastzetten", + "showUserDetails": "Show User Details", + "latestUnreadIsAbove": "Eerste ongelezen bericht staat boven", + "sendRecoveryPhraseTitle": "Herstelzin verzenden", + "sendRecoveryPhraseMessage": "Je probeert uw herstel zin te versturen welke kan worden gebruikt om toegang te krijgen tot jou account. Weet je zeker dat je dit bericht wilt versturen?", + "dialogClearAllDataDeletionFailedTitle": "Gegevens niet verwijderd", + "dialogClearAllDataDeletionFailedDesc": "Gegevens niet verwijderd door een onbekende fout. Wilt je gegevens verwijderen van alleen dit apparaat?", + "dialogClearAllDataDeletionFailedTitleQuestion": "Wilt je gegevens verwijderen van alleen dit apparaat?", + "dialogClearAllDataDeletionFailedMultiple": "Gegevens niet verwijderd door deze Service Nodes: $snodes$", + "dialogClearAllDataDeletionQuestion": "Wilt je alleen dit apparaat, of uw hele account verwijderen?", + "deviceOnly": "Alleen apparaat", + "entireAccount": "Gehele account", + "areYouSureDeleteDeviceOnly": "Weet je zeker dat je de gegevens van alleen uw apparaat wilt verwijderen?", + "areYouSureDeleteEntireAccount": "Weet je zeker dat je je hele account wilt verwijderen, inclusief de netwerkgegevens?", + "iAmSure": "Ik weet het zeker", + "recoveryPhraseSecureTitle": "Je bent bijna klaar!", + "recoveryPhraseRevealMessage": "Beveilig je account door je herstelzin op te slaan. Bekijk je herstelzin en sla deze veilig op om het te beveiligen.", + "recoveryPhraseRevealButtonText": "Toon herstelzin", + "notificationSubtitle": "Meldingen - $setting$", + "surveyTitle": "Neem deel aan onze Session enquête", + "goToOurSurvey": "Ga naar onze enquête", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Aanvaarden", + "decline": "Afwijzen", + "endCall": "Gesprek beëindigen", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Kan geen nieuw gesprek starten", + "callMissed": "Gemiste oproep van $name$", + "callMissedTitle": "Oproep gemist", + "noCameraFound": "Geen camera gevonden", + "noAudioInputFound": "Geen audio-invoer gevonden", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/no/messages.json b/_locales/no/messages.json index fa57f7c4c..832b8a086 100644 --- a/_locales/no/messages.json +++ b/_locales/no/messages.json @@ -1,460 +1,466 @@ { - "privacyPolicy": "Terms & Privacy Policy", - "copyErrorAndQuit": "Copy error and quit", - "unknown": "Unknown", - "databaseError": "Database Error", - "mainMenuFile": "&File", - "mainMenuEdit": "&Edit", - "mainMenuView": "&View", - "mainMenuWindow": "&Window", - "mainMenuHelp": "&Help", - "appMenuHide": "Hide", - "appMenuHideOthers": "Hide Others", - "appMenuUnhide": "Show All", - "appMenuQuit": "Quit Session", - "editMenuUndo": "Undo", - "editMenuRedo": "Redo", - "editMenuCut": "Cut", - "editMenuCopy": "Copy", - "editMenuPaste": "Paste", - "editMenuPasteAndMatchStyle": "Paste and Match Style", - "editMenuDelete": "Delete", - "editMenuSelectAll": "Select all", - "windowMenuClose": "Close Window", - "windowMenuMinimize": "Minimize", + "privacyPolicy": "Vilkår og personvernerklæring", + "copyErrorAndQuit": "Kopier feilmelding og avslutt", + "unknown": "Ukjent", + "databaseError": "Databasefeil", + "mainMenuFile": "&Fil", + "mainMenuEdit": "&Rediger", + "mainMenuView": "&Vis", + "mainMenuWindow": "&Vindu", + "mainMenuHelp": "&Hjelp", + "appMenuHide": "Skjul", + "appMenuHideOthers": "Skjul andre", + "appMenuUnhide": "Vis alle", + "appMenuQuit": "Avslutt Session", + "editMenuUndo": "Angre", + "editMenuRedo": "Gjenta", + "editMenuCut": "Klipp ut", + "editMenuCopy": "Kopier", + "editMenuPaste": "Lim inn", + "editMenuPasteAndMatchStyle": "Lim inn med gjeldende stil", + "editMenuDelete": "Slett", + "editMenuSelectAll": "Velg alt", + "windowMenuClose": "Lukk vindu", + "windowMenuMinimize": "Minimer", "windowMenuZoom": "Zoom", - "windowMenuBringAllToFront": "Bring All to Front", - "viewMenuResetZoom": "Actual Size", - "viewMenuZoomIn": "Zoom In", - "viewMenuZoomOut": "Zoom Out", - "viewMenuToggleFullScreen": "Toggle Full Screen", - "viewMenuToggleDevTools": "Toggle Developer Tools", - "contextMenuNoSuggestions": "No Suggestions", - "openGroupInvitation": "Open group invitation", - "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", - "enterSessionIDOrONSName": "Enter Session ID or ONS name", - "loading": "Loading...", - "optimizingApplication": "Optimizing application...", - "done": "Done", - "me": "Me", - "view": "View", - "youLeftTheGroup": "You have left the group.", - "youGotKickedFromGroup": "You were removed from the group.", - "unreadMessage": "Unread Message", - "unreadMessages": "Unread Messages", - "debugLogExplanation": "This log will be posted publicly online for contributors to view. You may examine and edit it before submitting.", - "debugLogError": "Something went wrong with the upload! Please consider manually adding your log to the bug you file.", - "reportIssue": "Report an issue", - "gotIt": "Got it", - "submit": "Submit", - "markAllAsRead": "Mark All as Read", - "incomingError": "Error handling incoming message", + "windowMenuBringAllToFront": "Bring alle til toppen", + "viewMenuResetZoom": "Opprinnelig størrelse", + "viewMenuZoomIn": "Zoom inn", + "viewMenuZoomOut": "Zoom ut", + "viewMenuToggleFullScreen": "Fullskjerm", + "viewMenuToggleDevTools": "Utviklerverktøy", + "contextMenuNoSuggestions": "Ingen forslag", + "openGroupInvitation": "Invitasjon til åpen gruppe", + "joinOpenGroupAfterInvitationConfirmationTitle": "Bli med i $roomName$?", + "joinOpenGroupAfterInvitationConfirmationDesc": "Er du sikker på at du vil bli med i den åpne gruppen $roomName$?", + "enterSessionIDOrONSName": "Angi Session-ID eller ONS-navn", + "loading": "Laster...", + "optimizingApplication": "Optimerer applikasjonen...", + "done": "Ferdig", + "me": "Meg", + "view": "Vis", + "youLeftTheGroup": "Du har forlatt gruppen.", + "youGotKickedFromGroup": "Du ble fjernet fra gruppen.", + "unreadMessage": "Ulest melding", + "unreadMessages": "Uleste meldinger", + "debugLogExplanation": "Denne loggen vil bli lagt ut offentlig på internett slik at bidragsytere kan se den. Du kan undersøke og redigere den før du sender den inn.", + "debugLogError": "Noe gikk galt med opplastingen! Vennligst vurder å manuelt legge loggen din til insektet du leverer inn.", + "reportIssue": "Rapporter et problem", + "gotIt": "Oppfattet", + "submit": "Send inn", + "markAllAsRead": "Merk alt som lest", + "incomingError": "Feil under behandling av innkommende beskjed", "media": "Media", - "mediaEmptyState": "No media", - "documents": "Documents", - "documentsEmptyState": "No documents", - "today": "Today", - "yesterday": "Yesterday", - "thisWeek": "This week", - "thisMonth": "This Month", - "voiceMessage": "Voice Message", - "dangerousFileType": "For security reasons, this file type cannot be sent", - "stagedPreviewThumbnail": "Draft thumbnail link preview for $domain$", - "previewThumbnail": "Thumbnail link preview for $domain$", - "stagedImageAttachment": "Draft image attachment: $path$", - "oneNonImageAtATimeToast": "Sorry, there is a limit of one non-image attachment per message", - "cannotMixImageAndNonImageAttachments": "Sorry, you cannot mix images with other file types in one message", - "maximumAttachments": "Maximum number of attachments reached. Please send remaining attachments in a separate message.", - "fileSizeWarning": "Attachment exceeds size limits for the type of message you're sending.", - "unableToLoadAttachment": "Sorry, there was an error setting your attachment.", - "offline": "Offline", - "checkNetworkConnection": "Check your network connection.", - "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", - "submitDebugLog": "Debug log", - "debugLog": "Debug Log", - "goToReleaseNotes": "Go to Release Notes", - "goToSupportPage": "Go to Support Page", - "menuReportIssue": "Report an Issue", - "about": "About", - "speech": "Speech", - "show": "Show", - "sessionMessenger": "Session", - "search": "Search", - "noSearchResults": "No results found for \"$searchTerm$\"", - "conversationsHeader": "Conversations", - "contactsHeader": "Contacts", - "messagesHeader": "Messages", - "settingsHeader": "Settings", - "typingAlt": "Typing animation for this conversation", - "contactAvatarAlt": "Avatar for contact $name$", - "downloadAttachment": "Download Attachment", - "replyToMessage": "Reply to message", - "replyingToMessage": "Replying to:", - "originalMessageNotFound": "Original message not found", - "originalMessageNotAvailable": "Original message no longer available", - "messageFoundButNotLoaded": "Original message found, but not loaded. Scroll up to load it.", - "recording": "Recording", - "you": "You", + "mediaEmptyState": "Ingen medier", + "documents": "Dokumenter", + "documentsEmptyState": "Ingen dokumenter", + "today": "I dag", + "yesterday": "I går", + "thisWeek": "Denne uken", + "thisMonth": "Denne måneden", + "voiceMessage": "Talebeskjed", + "dangerousFileType": "Av sikkerhetsårsaker kan denne filtypen ikke sendes", + "stagedPreviewThumbnail": "Miniatyrlenkeforhåndsvisningskladd for $domain$", + "previewThumbnail": "Miniatyrlenkeforhåndsvisning for $domain$", + "stagedImageAttachment": "Bildevedleggskladd: $path$", + "oneNonImageAtATimeToast": "Beklager, det er en begrensning på ett vedlegg utenom bilder per beskjed", + "cannotMixImageAndNonImageAttachments": "Beklager, du kan ikke blande bilder med andre filtyper i samme beskjed", + "maximumAttachments": "Maksimalt antall vedlegg nådd. Vennligst send gjenstående vedlegg i en adskilt beskjed.", + "fileSizeWarning": "Vedlegget overskrider størrelsesbegrensninger for beskjedtypen du sender.", + "unableToLoadAttachment": "Beklager, det skjedde en feil ved stilling av vedlegget ditt.", + "offline": "Frakoblet", + "checkNetworkConnection": "Kontroller nettverksforbindelsen din.", + "attemptingReconnection": "Forsøker å koble til på nytt om $reconnect_duration_in_seconds$ sekunder", + "submitDebugLog": "Feilsøkingslogg", + "debugLog": "Feilsøkingslogg", + "showDebugLog": "Show Debug Log", + "goToReleaseNotes": "Gå til utgivelsesmerknader", + "goToSupportPage": "Gå til støttesiden", + "menuReportIssue": "Rapporter et problem", + "about": "Om", + "speech": "Tale", + "show": "Vis", + "sessionMessenger": "Økt", + "search": "Søk", + "noSearchResults": "Ingen resultater funnet for «$searchTerm$»", + "conversationsHeader": "Samtaler", + "contactsHeader": "Kontakter", + "messagesHeader": "Beskjeder", + "settingsHeader": "Innstillinger", + "typingAlt": "Inntastingsanimasjon for denne samtalen", + "contactAvatarAlt": "Avatar for kontakt $name$", + "downloadAttachment": "Last ned vedlegg", + "replyToMessage": "Svar på beskjed", + "replyingToMessage": "Svarer på:", + "originalMessageNotFound": "Opprinnelig beskjed ikke funnet", + "originalMessageNotAvailable": "Opprinnelig beskjed er ikke lenger tilgjengelig", + "messageFoundButNotLoaded": "Opprinnelig beskjed funnet, men ikke lastet inn. Rull oppover for å laste den.", + "recording": "Tar opp", + "you": "Du", "audioPermissionNeededTitle": "Microphone access required", - "audioPermissionNeeded": "You can enable microphone access under: Settings (Gear icon) => Privacy", - "audio": "Audio", + "audioPermissionNeeded": "Du kan aktivere mikrofontilgang under: Innstillinger (Tannhjulssymbol) => Personvern", + "audio": "Lyd", "video": "Video", - "photo": "Photo", - "cannotUpdate": "Cannot Update", - "cannotUpdateDetail": "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", + "photo": "Fotografi", + "cannotUpdate": "Kan ikke oppdatere", + "cannotUpdateDetail": "Session Desktop mislyktes i å oppdatere, men det er en ny versjon tilgjengelig. Vennligst gå til https://getsession.org/ og installer den nye versjonen manuelt, og kontakt enten deretter kundestøtte eller lever inn et insekt om dette problemet.", "ok": "OK", - "cancel": "Cancel", - "close": "Close", - "continue": "Continue", - "error": "Error", - "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", - "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", - "deleted": "Deleted", - "from": "From:", - "to": "To:", - "sent": "Sent", - "received": "Received", - "sendMessage": "Message", - "groupMembers": "Group members", - "moreInformation": "More information", - "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", - "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", - "clearAllData": "Clear All Data", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", - "deleteContactConfirmation": "Are you sure you want to delete this conversation?", - "quoteThumbnailAlt": "Thumbnail of image from quoted message", - "imageAttachmentAlt": "Image attached to message", - "videoAttachmentAlt": "Screenshot of video attached to message", - "lightboxImageAlt": "Image sent in conversation", - "imageCaptionIconAlt": "Icon showing that this image has a caption", - "addACaption": "Add a caption...", - "copy": "Copy", - "copySessionID": "Copy Session ID", - "copyOpenGroupURL": "Copy Group's URL", - "save": "Save", - "saved": "Saved", - "permissions": "Permissions", - "general": "General", - "tookAScreenshot": "$name$ took a screenshot", - "savedTheFile": "Media saved by $name$", - "linkPreviewsTitle": "Send Link Previews", - "linkPreviewDescription": "Previews are supported for most urls", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", - "spellCheckTitle": "Spell Check", - "spellCheckDescription": "Enable spell check of text entered in message composition box", - "spellCheckDirty": "You must restart Session to apply your new settings", - "notifications": "Notifications", - "readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).", - "readReceiptSettingTitle": "Read Receipts", - "typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).", - "typingIndicatorsSettingTitle": "Typing Indicators", - "zoomFactorSettingTitle": "Zoom Factor", - "notificationSettingsDialog": "When messages arrive, display notifications that reveal...", - "disableNotifications": "Mute notifications", - "nameAndMessage": "Name and content", - "noNameOrMessage": "No name or content", - "nameOnly": "Name Only", - "newMessage": "New Message", - "newMessages": "New Messages", - "notificationMostRecentFrom": "Most recent from: $name$", - "notificationFrom": "From:", - "notificationMostRecent": "Most recent:", - "sendFailed": "Send Failed", - "expiredWarning": "This version of Session has expired. Please upgrade to the latest version to continue messaging.", - "upgrade": "Upgrade", - "mediaMessage": "Media message", - "timestamp_s": "Now", - "timestamp_m": "1 minute", - "timestamp_h": "1 hour", - "timestampFormat_M": "MMM D", - "messageBodyMissing": "Please enter a message body.", - "unblockToSend": "Unblock this contact to send a message.", - "unblockGroupToSend": "This group is blocked. Unlock it if you would like to send a message.", - "youChangedTheTimer": "You set the disappearing message timer to $time$", - "timerSetOnSync": "Updated disappearing message timer to $time$", - "theyChangedTheTimer": "$name$ set the disappearing message timer to $time$", - "timerOption_0_seconds": "Off", - "timerOption_5_seconds": "5 seconds", - "timerOption_10_seconds": "10 seconds", - "timerOption_30_seconds": "30 seconds", - "timerOption_1_minute": "1 minute", - "timerOption_5_minutes": "5 minutes", - "timerOption_30_minutes": "30 minutes", - "timerOption_1_hour": "1 hour", - "timerOption_6_hours": "6 hours", - "timerOption_12_hours": "12 hours", - "timerOption_1_day": "1 day", - "timerOption_1_week": "1 week", - "disappearingMessages": "Disappearing messages", - "changeNickname": "Change Nickname", - "clearNickname": "Clear nickname", - "nicknamePlaceholder": "New Nickname", - "changeNicknameMessage": "Enter a nickname for this user", - "timerOption_0_seconds_abbreviated": "off", + "cancel": "Avbryt", + "close": "Lukk", + "continue": "Fortsett", + "error": "Feil", + "delete": "Slett", + "messageDeletionForbidden": "Du har ikke tillatelse til å slette andres beskjeder", + "deleteJustForMe": "Slett kun for meg", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Slett beskjeder", + "deleted": "Slettet", + "messageDeletedPlaceholder": "Denne beskjeden er slettet", + "from": "Fra:", + "to": "Til:", + "sent": "Sendt", + "received": "Mottatt", + "sendMessage": "Beskjed", + "groupMembers": "Gruppemedlemmer", + "moreInformation": "Mer informasjon", + "resend": "Send på nytt", + "deleteConversationConfirmation": "Slett beskjedene permanent i denne samtalen?", + "clearAllData": "Fjern alle data", + "deleteAccountWarning": "Dette vil slette dine beskjeder og kontakter permanent.", + "deleteContactConfirmation": "Er du sikker på at du vil slette denne samtalen?", + "quoteThumbnailAlt": "Miniatyrbilde av bilde i sitert beskjed", + "imageAttachmentAlt": "Bilde vedlagt til beskjed", + "videoAttachmentAlt": "Skjermbilde av video vedlagt til beskjed", + "lightboxImageAlt": "Bilde sendt i samtale", + "imageCaptionIconAlt": "Symbol som viser at dette bildet har en bildetekst", + "addACaption": "Legg til bildetekst...", + "copy": "Kopier", + "copySessionID": "Kopier Session-ID", + "copyOpenGroupURL": "Kopier gruppens URL", + "save": "Lagre", + "saveLogToDesktop": "Save log to desktop", + "saved": "Lagret", + "permissions": "Tillatelser", + "general": "Generelt", + "tookAScreenshot": "$name$ tok et skjermbilde", + "savedTheFile": "Media lagret av $name$", + "linkPreviewsTitle": "Send forhåndsvisning av lenker", + "linkPreviewDescription": "Forhåndsvisninger støttes for de fleste nettadresser", + "linkPreviewsConfirmMessage": "Du vil ikke ha full metadatabeskyttelse når du sender lenkeforhåndsvisninger.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", + "spellCheckTitle": "Stavekontroll", + "spellCheckDescription": "Aktiver stavekontroll av tekst inntastet i feltet for forfatting av beskjeder", + "spellCheckDirty": "Du må starte Session på nytt for at dine nye innstillinger skal tre i kraft", + "notifications": "Varsler", + "readReceiptSettingDescription": "Se og del når beskjeder er lest (aktiverer lesekvitteringer i alle økter).", + "readReceiptSettingTitle": "Lesekvitteringer", + "typingIndicatorsSettingDescription": "Se og del når beskjeder blir skrevet (gjelder alle økter).", + "typingIndicatorsSettingTitle": "Inntastingsindikatorer", + "zoomFactorSettingTitle": "Zoomfaktor", + "notificationSettingsDialog": "Ved innkommende beskjeder, vis varsler som avslører:", + "disableNotifications": "Deaktiver varsler", + "nameAndMessage": "Navn og innhold", + "noNameOrMessage": "Verken navn eller innhold", + "nameOnly": "Kun navn", + "newMessage": "Ny beskjed", + "newMessages": "Nye beskjeder", + "notificationMostRecentFrom": "Siste fra: $name$", + "notificationFrom": "Fra:", + "notificationMostRecent": "Siste:", + "sendFailed": "Sending mislyktes", + "expiredWarning": "Denne versjonen av Session er foreldet. Vennligst oppgrader til den siste versjonen for å fortsette med å sende beskjeder.", + "upgrade": "Oppgrader", + "mediaMessage": "Mediebeskjed", + "timestamp_s": "Nå", + "timestamp_m": "1 minutt", + "timestamp_h": "1 time", + "timestampFormat_M": "D. MMM", + "messageBodyMissing": "Vennligst tast inn beskjedinnhold.", + "unblockToSend": "Avblokker denne kontakten for å sende en beskjed.", + "unblockGroupToSend": "Denne gruppen er blokkert. Avblokker den hvis du ønsker å sende en beskjed.", + "youChangedTheTimer": "Du satte utløpstiden for beskjeder til $time$", + "timerSetOnSync": "Oppdaterte utløpstiden for beskjeder til $time$", + "theyChangedTheTimer": "$name$ satte utløpstiden for beskjeder til $time$", + "timerOption_0_seconds": "Av", + "timerOption_5_seconds": "5 sekunder", + "timerOption_10_seconds": "10 sekunder", + "timerOption_30_seconds": "30 sekunder", + "timerOption_1_minute": "1 minutt", + "timerOption_5_minutes": "5 minutter", + "timerOption_30_minutes": "30 minutter", + "timerOption_1_hour": "1 time", + "timerOption_6_hours": "6 timer", + "timerOption_12_hours": "12 timer", + "timerOption_1_day": "1 dag", + "timerOption_1_week": "1 uke", + "disappearingMessages": "Tidsbegrensede beskjeder", + "changeNickname": "Forandre kallenavn", + "clearNickname": "Fjern kallenavn", + "nicknamePlaceholder": "Nytt kallenavn", + "changeNicknameMessage": "Skriv inn et kallenavn for denne brukeren", + "timerOption_0_seconds_abbreviated": "av", "timerOption_5_seconds_abbreviated": "5s", "timerOption_10_seconds_abbreviated": "10s", "timerOption_30_seconds_abbreviated": "30s", "timerOption_1_minute_abbreviated": "1m", "timerOption_5_minutes_abbreviated": "5m", - "timerOption_30_minutes_abbreviated": "30m", - "timerOption_1_hour_abbreviated": "1h", - "timerOption_6_hours_abbreviated": "6h", - "timerOption_12_hours_abbreviated": "12h", + "timerOption_30_minutes_abbreviated": "30min", + "timerOption_1_hour_abbreviated": "1t", + "timerOption_6_hours_abbreviated": "6t", + "timerOption_12_hours_abbreviated": "12t", "timerOption_1_day_abbreviated": "1d", - "timerOption_1_week_abbreviated": "1w", - "disappearingMessagesDisabled": "Disappearing messages disabled", - "disabledDisappearingMessages": "$name$ disabled disappearing messages.", - "youDisabledDisappearingMessages": "You disabled disappearing messages.", - "timerSetTo": "Disappearing message time set to $time$", - "noteToSelf": "Note to Self", - "hideMenuBarTitle": "Hide Menu Bar", - "hideMenuBarDescription": "Toggle system menu bar visibility", - "startConversation": "Start New Conversation", - "invalidNumberError": "Invalid Session ID or ONS Name", - "failedResolveOns": "Failed to resolve ONS name", - "successUnlinked": "Your device was unlinked successfully", - "autoUpdateSettingTitle": "Auto Update", - "autoUpdateSettingDescription": "Automatically check for updates on launch", - "autoUpdateNewVersionTitle": "Session update available", - "autoUpdateNewVersionMessage": "There is a new version of Session available.", - "autoUpdateNewVersionInstructions": "Press Restart Session to apply the updates.", - "autoUpdateRestartButtonLabel": "Restart Session", - "autoUpdateLaterButtonLabel": "Later", - "autoUpdateDownloadButtonLabel": "Download", - "autoUpdateDownloadedMessage": "The new update has been downloaded.", - "autoUpdateDownloadInstructions": "Would you like to download the update?", - "leftTheGroup": "$name$ has left the group.", - "multipleLeftTheGroup": "$name$ left the group", - "updatedTheGroup": "Group updated", - "titleIsNow": "Group name is now '$name$'.", - "joinedTheGroup": "$name$ joined the group.", - "multipleJoinedTheGroup": "$name$ joined the group.", - "kickedFromTheGroup": "$name$ was removed from the group.", - "multipleKickedFromTheGroup": "$name$ were removed from the group.", - "blockUser": "Block", - "unblockUser": "Unblock", - "unblocked": "Unblocked", - "blocked": "Blocked", - "blockedSettingsTitle": "Blocked contacts", - "unbanUser": "Unban User", - "unbanUserConfirm": "Are you sure you want to unban user?", - "userUnbanned": "User unbanned successfully", - "userUnbanFailed": "Unban failed!", - "banUser": "Ban User", - "banUserConfirm": "Are you sure you want to ban user?", - "banUserAndDeleteAll": "Ban and Delete All", - "banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?", - "userBanned": "User banned successfully", - "userBanFailed": "Ban failed!", - "leaveGroup": "Leave Group", - "leaveAndRemoveForEveryone": "Leave Group and remove for everyone", - "leaveGroupConfirmation": "Are you sure you want to leave this group?", - "leaveGroupConfirmationAdmin": "As you are the admin of this group, if you leave it it will be removed for every current members. Are you sure you want to leave this group?", - "cannotRemoveCreatorFromGroup": "Cannot remove this user", - "cannotRemoveCreatorFromGroupDesc": "You cannot remove this user as they are the creator of the group.", - "noContactsForGroup": "You don't have any contacts yet", - "failedToAddAsModerator": "Failed to add user as moderator", - "failedToRemoveFromModerator": "Failed to remove user from the moderator list", - "copyMessage": "Copy message text", - "selectMessage": "Select message", - "editGroup": "Edit group", - "editGroupName": "Edit group name", - "updateGroupDialogTitle": "Updating $name$...", - "showRecoveryPhrase": "Recovery Phrase", - "yourSessionID": "Your Session ID", - "setAccountPasswordTitle": "Set Account Password", - "setAccountPasswordDescription": "Require password to unlock Session’s screen. You can still receive message notifications while Screen Lock is enabled. Session’s notification settings allow you to customize information that is displayed", - "changeAccountPasswordTitle": "Change Account Password", - "changeAccountPasswordDescription": "Change your password", - "removeAccountPasswordTitle": "Remove Account Password", - "removeAccountPasswordDescription": "Remove the password associated with your account", - "enterPassword": "Please enter your password", - "confirmPassword": "Confirm password", - "pasteLongPasswordToastTitle": "The clipboard content exceeds the maximum password length of $max_pwd_len$ characters.", - "showRecoveryPhrasePasswordRequest": "Please enter your password", - "recoveryPhraseSavePromptMain": "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don't give it to anyone.", - "invalidOpenGroupUrl": "Invalid URL", - "copiedToClipboard": "Copied to clipboard", - "passwordViewTitle": "Type In Your Password", - "unlock": "Unlock", - "password": "Password", - "setPassword": "Set Password", - "changePassword": "Change Password", - "removePassword": "Remove Password", - "maxPasswordAttempts": "Invalid Password. Would you like to reset the database?", - "typeInOldPassword": "Please type in your old password", - "invalidOldPassword": "Old password is invalid", - "invalidPassword": "Invalid password", - "noGivenPassword": "Please enter your password", - "passwordsDoNotMatch": "Passwords do not match", - "setPasswordInvalid": "Passwords do not match", - "changePasswordInvalid": "The old password you entered is incorrect", - "removePasswordInvalid": "Incorrect password", - "setPasswordTitle": "Set Password", - "changePasswordTitle": "Changed Password", - "removePasswordTitle": "Removed Password", - "setPasswordToastDescription": "Your password has been set. Please keep it safe.", - "changePasswordToastDescription": "Your password has been changed. Please keep it safe.", - "removePasswordToastDescription": "You have removed your password.", - "publicChatExists": "You are already connected to this open group", - "connectToServerFail": "Couldn't join group", - "connectingToServer": "Connecting...", - "connectToServerSuccess": "Successfully connected to open group", - "setPasswordFail": "Failed to set password", - "passwordLengthError": "Password must be between 6 and 64 characters long", - "passwordTypeError": "Password must be a string", - "passwordCharacterError": "Password must only contain letters, numbers and symbols", - "remove": "Remove", - "invalidSessionId": "Invalid Session ID", - "invalidPubkeyFormat": "Invalid Pubkey Format", - "emptyGroupNameError": "Please enter a group name", - "editProfileModalTitle": "Profile", - "groupNamePlaceholder": "Group Name", - "inviteContacts": "Invite Contacts", - "addModerators": "Add Moderators", - "removeModerators": "Remove Moderators", - "addAsModerator": "Add As Moderator", - "removeFromModerators": "Remove From Moderators", - "add": "Add", - "addingContacts": "Adding contacts to", - "noContactsToAdd": "No contacts to add", - "noMembersInThisGroup": "No other members in this group", - "noModeratorsToRemove": "no moderators to remove", - "onlyAdminCanRemoveMembers": "You are not the creator", - "onlyAdminCanRemoveMembersDesc": "Only the creator of the group can remove users", - "createAccount": "Create Account", - "signIn": "Sign In", - "startInTrayTitle": "Start in Tray", - "startInTrayDescription": "Start Session as a minified app ", - "yourUniqueSessionID": "Say hello to your Session ID", - "allUsersAreRandomly...": "Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design.", - "getStarted": "Get started", - "createSessionID": "Create Session ID", - "recoveryPhrase": "Recovery Phrase", - "enterRecoveryPhrase": "Enter your recovery phrase", - "displayName": "Display Name", - "anonymous": "Anonymous", - "removeResidueMembers": "Clicking ok will also remove those members as they left the group.", - "enterDisplayName": "Enter a display name", - "enterOptionalPassword": "Enter password (optional)", - "continueYourSession": "Continue Your Session", - "linkDevice": "Link Device", - "restoreUsingRecoveryPhrase": "Restore your account", - "or": "or", - "ByUsingThisService...": "By using this service, you agree to our Terms of Service and Privacy Policy", - "beginYourSession": "Begin your Session.", - "welcomeToYourSession": "Welcome to your Session", - "newSession": "New Session", - "searchFor...": "Search for conversations or contacts", - "enterSessionID": "Enter Session ID", - "enterSessionIDOfRecipient": "Enter Session ID or ONS name of recipient", - "usersCanShareTheir...": "Users can share their Session ID by going into their account settings and tapping \"Share Session ID\", or by sharing their QR code.", - "message": "Message", - "appearanceSettingsTitle": "Appearance", - "permissionSettingsTitle": "Permissions", - "privacySettingsTitle": "Privacy", - "notificationsSettingsTitle": "Notifications", - "recoveryPhraseEmpty": "Enter your recovery phrase", - "displayNameEmpty": "Please pick a display name", - "members": "$count$ members", - "joinOpenGroup": "Join Open Group", - "newClosedGroup": "New Closed Group", - "createClosedGroupNamePrompt": "Group Name", - "createClosedGroupPlaceholder": "Enter a group name", - "openGroupURL": "Open Group URL", - "enterAnOpenGroupURL": "Enter an open group URL", - "next": "Next", - "description": "Description", - "invalidGroupNameTooShort": "Please enter a group name", - "invalidGroupNameTooLong": "Please enter a shorter group name", - "pickClosedGroupMember": "Please pick at least 1 group member", - "closedGroupMaxSize": "A closed group cannot have more than 100 members", - "noBlockedContacts": "No blocked contacts", - "userAddedToModerators": "User added to moderator list", - "userRemovedFromModerators": "User removed from moderator list", - "orJoinOneOfThese": "Or join one of these...", - "helpUsTranslateSession": "Help us Translate Session", - "translation": "Translation", - "closedGroupInviteFailTitle": "Group Invitation Failed", - "closedGroupInviteFailTitlePlural": "Group Invitations Failed", - "closedGroupInviteFailMessage": "Unable to successfully invite a group member", - "closedGroupInviteFailMessagePlural": "Unable to successfully invite all group members", - "closedGroupInviteOkText": "Retry invitations", - "closedGroupInviteSuccessTitlePlural": "Group Invitations Completed", - "closedGroupInviteSuccessTitle": "Group Invitation Succeeded", - "closedGroupInviteSuccessMessage": "Successfully invited closed group members", - "notificationForConvo": "Notifications", - "notificationForConvo_all": "All", - "notificationForConvo_disabled": "Disabled", - "notificationForConvo_mentions_only": "Mentions only", - "onionPathIndicatorTitle": "Path", - "onionPathIndicatorDescription": "Session hides your IP by bouncing your messages through several Service Nodes in Session's decentralized network. These are the countries your connection is currently being bounced through:", - "unknownCountry": "Unknown Country", - "device": "Device", - "destination": "Destination", - "learnMore": "Learn more", - "linkVisitWarningTitle": "Open this link in your browser?", - "linkVisitWarningMessage": "Are you sure you want to open $url$ in your browser?", - "open": "Open", - "audioMessageAutoplayTitle": "Audio Message Autoplay", - "audioMessageAutoplayDescription": "Automatically play consecutively sent audio messages", - "clickToTrustContact": "Click to download media", - "trustThisContactDialogTitle": "Trust $name$?", - "trustThisContactDialogDescription": "Are you sure you want to download media sent by $name$?", - "pinConversation": "Pin Conversation", - "unpinConversation": "Unpin Conversation", - "pinConversationLimitTitle": "Pinned conversations limit", - "pinConversationLimitToastDescription": "You can only pin $number$ conversations", - "latestUnreadIsAbove": "First unread message is above", - "sendRecoveryPhraseTitle": "Sending Recovery Phrase", - "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", - "dialogClearAllDataDeletionFailedTitle": "Data not deleted", - "dialogClearAllDataDeletionFailedDesc": "Data not deleted with an unknown error. Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedTitleQuestion": "Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", - "dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?", - "deviceOnly": "Device Only", - "entireAccount": "Entire Account", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", - "recoveryPhraseSecureTitle": "You're almost finished!", - "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", - "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", - "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "timerOption_1_week_abbreviated": "1u", + "disappearingMessagesDisabled": "Tidsbegrensede beskjeder er deaktivert", + "disabledDisappearingMessages": "$name$ deaktiverte tidsbegrensede beskjeder.", + "youDisabledDisappearingMessages": "Du deaktiverte tidsbegrensede beskjeder.", + "timerSetTo": "Utløpstiden for beskjeder satt til $time$", + "noteToSelf": "Notat til meg selv", + "hideMenuBarTitle": "Skjul menylinjen", + "hideMenuBarDescription": "Vis eller skjul menylinjen", + "startConversation": "Start ny samtale", + "invalidNumberError": "Ugyldig Session-ID eller ONS-navn", + "failedResolveOns": "Fant ikke ONS-navnet", + "successUnlinked": "Lyktes med å fjerne sammenkoblingen med enheten", + "autoUpdateSettingTitle": "Automatisk oppdatering", + "autoUpdateSettingDescription": "Se etter oppdateringer automatisk ved oppstart", + "autoUpdateNewVersionTitle": "Session-oppdatering tilgjengelig", + "autoUpdateNewVersionMessage": "En ny versjon av Session er tilgjengelig.", + "autoUpdateNewVersionInstructions": "Trykk på Start på nytt for å fullføre oppdateringene.", + "autoUpdateRestartButtonLabel": "Start på nytt", + "autoUpdateLaterButtonLabel": "Senere", + "autoUpdateDownloadButtonLabel": "Last ned", + "autoUpdateDownloadedMessage": "Oppdatering har blitt lastet ned.", + "autoUpdateDownloadInstructions": "Vil du laste ned oppdateringen?", + "leftTheGroup": "$name$ har forlatt gruppen.", + "multipleLeftTheGroup": "$name$ forlot gruppen", + "updatedTheGroup": "Gruppe oppdatert", + "titleIsNow": "Gruppens navn er nå '$name$'.", + "joinedTheGroup": "$name$ ble med i gruppen.", + "multipleJoinedTheGroup": "$name$ ble med i gruppen.", + "kickedFromTheGroup": "$name$ ble fjernet fra gruppen.", + "multipleKickedFromTheGroup": "$name$ ble fjernet fra gruppen.", + "blockUser": "Blokker", + "unblockUser": "Avblokker", + "unblocked": "Blokkering opphevet", + "blocked": "Blokkert", + "blockedSettingsTitle": "Blokkerte kontakter", + "unbanUser": "Opphev utestengelse av bruker", + "unbanUserConfirm": "Er du sikker på at du vil oppheve utestengelsen av brukeren?", + "userUnbanned": "Bruker ikke lenger utestengt", + "userUnbanFailed": "Oppheving av utestengelse mislyktes!", + "banUser": "Utesteng bruker", + "banUserConfirm": "Er du sikker på at du vil utestenge brukeren?", + "banUserAndDeleteAll": "Utesteng og slett alle", + "banUserAndDeleteAllConfirm": "Er du sikker på at du vil utestenge brukeren og slette alle beskjedene hans?", + "userBanned": "Lyktes med å utestenge brukeren", + "userBanFailed": "Utestengelse mislyktes!", + "leaveGroup": "Forlat gruppen", + "leaveAndRemoveForEveryone": "Forlat gruppen og fjern for alle", + "leaveGroupConfirmation": "Er du sikker på at du vil forlate denne gruppen?", + "leaveGroupConfirmationAdmin": "Ettersom du er administrator for denne gruppen, vil den dersom du forlater den fjernes for alle nåværende medlemmer. Er du sikker på at du vil forlate denne gruppen?", + "cannotRemoveCreatorFromGroup": "Kan ikke fjerne denne brukeren", + "cannotRemoveCreatorFromGroupDesc": "Du kan ikke fjerne denne brukeren, ettersom vedkommende er skaperen av gruppen.", + "noContactsForGroup": "Du har ingen kontakter ennå", + "failedToAddAsModerator": "Mislyktes i å legge til bruker som ordstyrer", + "failedToRemoveFromModerator": "Mislyktes i å fjerne bruker fra listen over ordstyrere", + "copyMessage": "Kopier beskjedtekst", + "selectMessage": "Velg beskjed", + "editGroup": "Rediger gruppe", + "editGroupName": "Rediger gruppenavn", + "updateGroupDialogTitle": "Oppdaterer $name$...", + "showRecoveryPhrase": "Gjenopprettelsesfrase", + "yourSessionID": "Din Session-ID", + "setAccountPasswordTitle": "Still kontopassord", + "setAccountPasswordDescription": "Krev passord for å låse opp Session-skjermen. Du kan fremdeles motta beskjedvarsler når skjermlås er aktivert. Sessions varslingsinnstillinger lar deg tilpasse hvilke opplysninger som vises", + "changeAccountPasswordTitle": "Forandre kontopassord", + "changeAccountPasswordDescription": "Forandre passordet ditt", + "removeAccountPasswordTitle": "Fjern kontopassord", + "removeAccountPasswordDescription": "Fjern passordet forbundet med kontoen din", + "enterPassword": "Vennligst skriv inn passordet ditt", + "confirmPassword": "Bekreft passordet", + "pasteLongPasswordToastTitle": "Innholdet av utklippstavlen overskrider den maksimale passordlengden på $max_pwd_len$ tegn.", + "showRecoveryPhrasePasswordRequest": "Vennligst skriv inn passordet ditt", + "recoveryPhraseSavePromptMain": "Gjenopprettelsesfrasen din er hovednøkkelen til Session-IDen din – du kan bruke den for å gjenopprette Session-IDen din dersom du mister tilgang til enheten din. Arkiver gjenopprettelsesfrasen din på et trygt sted, og ikke gi den til noen.", + "invalidOpenGroupUrl": "Ugyldig URL", + "copiedToClipboard": "Kopiert til utklippstavlen", + "passwordViewTitle": "Tast inn passordet ditt", + "unlock": "Lås opp", + "password": "Passord", + "setPassword": "Still passord", + "changePassword": "Forandre passord", + "removePassword": "Fjern passord", + "maxPasswordAttempts": "Ugyldig passord. Vil du nullstille databasen?", + "typeInOldPassword": "Vennligst tast inn det gamle passordet ditt", + "invalidOldPassword": "Gammelt passord er ugyldig", + "invalidPassword": "Ugyldig passord", + "noGivenPassword": "Vennligst skriv inn passordet ditt", + "passwordsDoNotMatch": "Passordene stemmer ikke overens", + "setPasswordInvalid": "Passordene stemmer ikke overens", + "changePasswordInvalid": "Det gamle passordet du skrev inn, var galt", + "removePasswordInvalid": "Galt passord", + "setPasswordTitle": "Stilte passordet", + "changePasswordTitle": "Forandret passordet", + "removePasswordTitle": "Fjernet passordet", + "setPasswordToastDescription": "Passordet er blitt stilt. Vennligst oppbevar det trygt.", + "changePasswordToastDescription": "Passordet ditt er endret. Vennligst oppbevar det trygt.", + "removePasswordToastDescription": "Du har fjernet passordet ditt.", + "publicChatExists": "Du er allerede koblet til denne åpne gruppen", + "connectToServerFail": "Kunne ikke bli med i gruppen", + "connectingToServer": "Kobler til...", + "connectToServerSuccess": "Lyktes med å koble til den åpne gruppen", + "setPasswordFail": "Kunne ikke stille passordet", + "passwordLengthError": "Passordet må være mellom 6 og 64 tegn langt", + "passwordTypeError": "Passordet må være en streng", + "passwordCharacterError": "Passordet kan kun inneholde bokstaver, tall og symboler", + "remove": "Fjern", + "invalidSessionId": "Ugyldig Session-ID", + "invalidPubkeyFormat": "Ugyldig format på offentlig nøkkel", + "emptyGroupNameError": "Vennligst skriv inn et gruppenavn", + "editProfileModalTitle": "Profil", + "groupNamePlaceholder": "Gruppenavn", + "inviteContacts": "Innby kontakter", + "addModerators": "Legg til ordstyrere", + "removeModerators": "Fjern ordstyrere", + "addAsModerator": "Legg til som ordstyrer", + "removeFromModerators": "Fjern fra ordstyrere", + "add": "Legg til", + "addingContacts": "Legger kontakter til", + "noContactsToAdd": "Ingen kontakter å legge til", + "noMembersInThisGroup": "Ingen andre medlemmer i denne gruppen", + "noModeratorsToRemove": "Ingen ordstyrere å fjerne", + "onlyAdminCanRemoveMembers": "Du er ikke skaperen av gruppen", + "onlyAdminCanRemoveMembersDesc": "Kun skaperen av gruppen kan fjerne brukere", + "createAccount": "Opprett konto", + "signIn": "Logg på", + "startInTrayTitle": "Start i varslingsfeltet", + "startInTrayDescription": "Start Session som minimert applikasjon", + "yourUniqueSessionID": "Si hei til din Session-ID", + "allUsersAreRandomly...": "Din Session-ID er den unike adressen folk kan bruke for å kontakte deg på Session. Uten en forbindelse til din virkelige identitet er din Session-ID laget for å være fullstendig anonym og privat.", + "getStarted": "Kom i gang", + "createSessionID": "Opprett Session-ID", + "recoveryPhrase": "Gjenopprettelsesfrase", + "enterRecoveryPhrase": "Skriv inn din gjenopprettelsesfrase", + "displayName": "Navn som vises", + "anonymous": "Anonym", + "removeResidueMembers": "Ved å klikke OK vil også disse medlemmene fjernes, ettersom de forlot gruppen.", + "enterDisplayName": "Skriv inn navnet som skal vises", + "enterOptionalPassword": "Skriv inn passord (valgfritt)", + "continueYourSession": "Fortsett økten din", + "linkDevice": "Koble sammen enhet", + "restoreUsingRecoveryPhrase": "Gjenopprett kontoen din", + "or": "eller", + "ByUsingThisService...": "Ved å bruke denne tjenesten, samtykker du i våre tjenestevilkår og personvernserklæring", + "beginYourSession": "Start økten din.", + "welcomeToYourSession": "Velkommen til økten din", + "newSession": "Ny økt", + "searchFor...": "Søk etter samtaler eller kontakter", + "enterSessionID": "Skriv inn Session-ID", + "enterSessionIDOfRecipient": "Skriv inn mottakerens Session-ID eller ONS-navn", + "usersCanShareTheir...": "Brukere kan dele sin Session-ID ved å gå inn i sine kontoinnstillinger og trykke på \"Del Session-ID\", eller ved å dele sin QR-kode.", + "message": "Beskjed", + "appearanceSettingsTitle": "Utseende", + "permissionSettingsTitle": "Tillatelser", + "privacySettingsTitle": "Personvern", + "notificationsSettingsTitle": "Varsler", + "recoveryPhraseEmpty": "Skriv inn gjenopprettelsesfrasen", + "displayNameEmpty": "Vennligst skriv inn navnet som skal vises", + "members": "$count$ medlemmer", + "joinOpenGroup": "Bli med i åpen gruppe", + "newClosedGroup": "Ny lukket gruppe", + "createClosedGroupNamePrompt": "Gruppenavn", + "createClosedGroupPlaceholder": "Skriv inn et gruppenavn", + "openGroupURL": "URL for åpen gruppe", + "enterAnOpenGroupURL": "Skriv inn en åpen gruppe-URL", + "next": "Neste", + "description": "1 minutt", + "invalidGroupNameTooShort": "Vennligst skriv inn et gruppenavn", + "invalidGroupNameTooLong": "Vennligst skriv inn et kortere gruppenavn", + "pickClosedGroupMember": "Vennligst velg minst 1 gruppemedlem", + "closedGroupMaxSize": "En lukket gruppe kan ikke ha flere enn 100 medlemmer", + "noBlockedContacts": "Ingen blokkerte kontakter", + "userAddedToModerators": "Bruker lagt til i listen over ordstyrere", + "userRemovedFromModerators": "Bruker fjernet fra listen over ordstyrere", + "orJoinOneOfThese": "Eller bli med i en av disse...", + "helpUsTranslateSession": "Hjelp oss med å oversette Session", + "translation": "Oversettelse", + "closedGroupInviteFailTitle": "Gruppeinnbydelse mislyktes", + "closedGroupInviteFailTitlePlural": "Gruppeinnbydelser mislyktes", + "closedGroupInviteFailMessage": "Klarte ikke å lykkes med å innby et gruppemedlem", + "closedGroupInviteFailMessagePlural": "Klarte ikke å lykkes med å innby alle gruppemedlemmene", + "closedGroupInviteOkText": "Forsøk innbydelser på nytt", + "closedGroupInviteSuccessTitlePlural": "Gruppeinnbydelser fullført", + "closedGroupInviteSuccessTitle": "Gruppeinnbydelse lyktes", + "closedGroupInviteSuccessMessage": "Lyktes med å innby medlemmer til lukket gruppe", + "notificationForConvo": "Varsler", + "notificationForConvo_all": "Alle", + "notificationForConvo_disabled": "Deaktivert", + "notificationForConvo_mentions_only": "Kun omtaler", + "onionPathIndicatorTitle": "Sti", + "onionPathIndicatorDescription": "Session skjuler IPen din ved å la beskjedene dine passere gjennom flere tjenesteknutepunkter i Sessions desentraliserte nettverk. Dette er landene forbindelsen din nå passerer gjennom:", + "unknownCountry": "Ukjent land", + "device": "Enhet", + "destination": "Mål", + "learnMore": "Lær mer", + "linkVisitWarningTitle": "Åpne denne lenken i nettleseren din?", + "linkVisitWarningMessage": "Er du sikker på at du vil åpne $url$ i nettleseren din?", + "open": "Åpne", + "audioMessageAutoplayTitle": "Automatisk avspilling av lydbeskjeder", + "audioMessageAutoplayDescription": "Spill automatisk av påfølgende sendte lydbeskjeder", + "clickToTrustContact": "Klikk for å laste ned medie", + "trustThisContactDialogTitle": "Stol på $name$?", + "trustThisContactDialogDescription": "Er du sikker på at du vil laste ned medier sendt av $name$?", + "pinConversation": "Fest samtale", + "unpinConversation": "Løsne samtale", + "pinConversationLimitTitle": "Begresning på festede samtaler", + "pinConversationLimitToastDescription": "Du kan kun feste $number$ samtaler", + "showUserDetails": "Show User Details", + "latestUnreadIsAbove": "Første uleste beskjed er ovenfor", + "sendRecoveryPhraseTitle": "Sender gjenopprettelsesfrase", + "sendRecoveryPhraseMessage": "Du forsøker å sende gjenopprettelsesfrasen din, som kan brukes til å få tilgang til kontoen din. Er du sikker på at du vil sende denne beskjeden?", + "dialogClearAllDataDeletionFailedTitle": "Opplysninger ikke slettet", + "dialogClearAllDataDeletionFailedDesc": "Opplysningene ble ikke slettet på grunn av en ukjent feil. Vil du slette opplysningene kun fra denne enheten?", + "dialogClearAllDataDeletionFailedTitleQuestion": "Vil du slette opplysningene kun fra denne enheten?", + "dialogClearAllDataDeletionFailedMultiple": "Opplysninger ikke slettet av disse tjenesteknutepunktene: $snodes$", + "dialogClearAllDataDeletionQuestion": "Ønsker du å rense kun denne enheten, eller slette hele kontoen din?", + "deviceOnly": "Kun enhet", + "entireAccount": "Hele kontoen", + "areYouSureDeleteDeviceOnly": "Er du sikker på at du kun vil slette opplysningene på enheten?", + "areYouSureDeleteEntireAccount": "Er du sikker på at du vil slette hele kontoen din, inkludert de nettlagrede opplysningene?", + "iAmSure": "Jeg er sikker", + "recoveryPhraseSecureTitle": "Du er nesten ferdig!", + "recoveryPhraseRevealMessage": "Sikre kontoen din ved å lagre gjenopprettelsesfrasen din. Vis gjenopprettelsesfrasen din, og arkiver den deretter trygt for å sikre den.", + "recoveryPhraseRevealButtonText": "Vis gjenopprettelsesfrase", + "notificationSubtitle": "Varsler – $setting$", + "surveyTitle": "Fyll ut vår Session-undersøkelse", + "goToOurSurvey": "Gå til undersøkelsen vår", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Godta", + "decline": "Avvis", + "endCall": "Avslutt anrop", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Kan ikke foreta nytt anrop", + "callMissed": "Tapt anrop fra $name$", + "callMissedTitle": "Anrop tapt", + "noCameraFound": "Intet kamera funnet", + "noAudioInputFound": "Ingen lyd-inndataenhet funnet", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/pa/messages.json b/_locales/pa/messages.json index fa57f7c4c..8428cbf5a 100644 --- a/_locales/pa/messages.json +++ b/_locales/pa/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Error", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Sent", @@ -124,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Save", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index f26fed539..aeccfc1e5 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Ponowne połączenie za $reconnect_duration_in_seconds$ sekund(-y)", "submitDebugLog": "Wyślij log debugowania", "debugLog": "Dziennik Debugowania", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Przejdź do informacji o wersji", "goToSupportPage": "Przejdź do strony wsparcia", "menuReportIssue": "Zgłoś problem", @@ -109,13 +110,14 @@ "continue": "Kontynuuj", "error": "Błąd", "delete": "Usuń", - "deletePublicWarning": "Czy na pewno? Spowoduje to permanentne usunięcie tej wiadomości dla każdego w tej otwartej grupie.", - "deleteMultiplePublicWarning": "Czy na pewno? Spowoduje to permanentne usunięcie tych wiadomości dla każdego w tej otwartej grupie.", - "deleteWarning": "Na pewno? Kliknięcie 'usuń' spowoduje bezpowrotne usunięcie wiadomości z tego urządzenia.", - "deleteMultipleWarning": "Czy na pewno? Kliknięcie 'usuń' spowoduje permanentne usunięcie wiadomości tylko z twojego urządzenia.", "messageDeletionForbidden": "Nie masz uprawnień do usunięcia wiadomości innych", - "deleteThisMessage": "Usuń wiadomość", + "deleteJustForMe": "Usuń tylko dla mnie", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Usuń konwersację", "deleted": "Usunięte", + "messageDeletedPlaceholder": "Ta wiadomość została usunięta", "from": "Od:", "to": "Do:", "sent": "Wysłano", @@ -124,17 +126,9 @@ "groupMembers": "Członkowie grupy", "moreInformation": "Więcej informacji", "resend": "Wyślij ponownie", - "deleteMessage": "Usuń wiadomość", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Usuń konwersację", - "deleteMessageForEveryone": "Usuń wiadomość dla wszystkich", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Usuń wiadomość dla wszystkich", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Usunąć trwale tę konwersację?", "clearAllData": "Wyczyść wszystkie dane", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Spowoduje to trwałe usunięcie wiadomości i kontaktów.", "deleteContactConfirmation": "Czy na pewno chcesz usunąć tę rozmowę?", "quoteThumbnailAlt": "Miniatura obrazu z cytowanej wiadomości", "imageAttachmentAlt": "Zdjęcie załączone do wiadomości", @@ -146,6 +140,7 @@ "copySessionID": "Skopiuj Session ID", "copyOpenGroupURL": "Skopiuj adres URL grupy", "save": "Zapisz", + "saveLogToDesktop": "Save log to desktop", "saved": "Zapisano", "permissions": "Uprawnienia", "general": "Ogólne", @@ -153,9 +148,9 @@ "savedTheFile": "Media zapisane przez $name$", "linkPreviewsTitle": "Podgląd linków", "linkPreviewDescription": "Previews are supported for most urls.", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofon i aparat", - "mediaPermissionsDescription": "Zezwól na dostęp do kamery i mikrofonu", + "linkPreviewsConfirmMessage": "Pełna ochrona metadanych nie będzie dostępna podczas wysyłania podglądu.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Sprawdzanie pisowni", "spellCheckDescription": "Włącz sprawdzanie pisowni podczas pisania wiadomości", "spellCheckDirty": "Musisz zrestartować Session, aby zastosować nowe ustawienia", @@ -421,6 +416,7 @@ "unpinConversation": "Odepnij konwersację", "pinConversationLimitTitle": "Limit przypiętych konwersacji", "pinConversationLimitToastDescription": "Możesz przypiąć tylko $number$ konwersacji", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Pierwsza nieprzeczytana wiadomość jest powyżej", "sendRecoveryPhraseTitle": "Wysyłanie frazy odzyskiwania", "sendRecoveryPhraseMessage": "Próbujesz wysłać frazę odzyskiwania, która może być użyta do uzyskania dostępu do twojego konta. Czy na pewno chcesz wysłać tę wiadomość?", @@ -431,30 +427,40 @@ "dialogClearAllDataDeletionQuestion": "Czy chcesz wyczyścić tylko to urządzenie, czy usunąć całe swoje konto?", "deviceOnly": "Tylko urządzenie", "entireAccount": "Całe konto", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "areYouSureDeleteDeviceOnly": "Czy na pewno chcesz usunąć swoje dane tylko z tego urządzenia?", + "areYouSureDeleteEntireAccount": "Czy na pewno chcesz usunąć całe swoje konto, w tym dane sieciowe?", + "iAmSure": "Na pewno", "recoveryPhraseSecureTitle": "Prawie gotowe!", "recoveryPhraseRevealMessage": "Zabezpiecz swoje konto zapisując frazę odzyskiwania. Zobacz frazę odzyskiwania, a następnie przechowuj ją bezpiecznie w celu zabezpieczenia.", "recoveryPhraseRevealButtonText": "Pokaż frazę odzyskiwania", "notificationSubtitle": "Powiadomienia - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Weź udział w ankiecie Session", + "goToOurSurvey": "Przejdź do naszej ankiety", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Akceptuj", + "decline": "Odrzuć", + "endCall": "Zakończ rozmowę", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Nie można rozpocząć nowego połączenia", + "callMissed": "Nieodebrane połączenie od $name$", + "callMissedTitle": "Połączenie nieodebrane", + "noCameraFound": "Nie znaleziono kamery", + "noAudioInputFound": "Nie znaleziono wejścia dźwięku", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 90022df64..8ed2bfc5e 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Tentando reconectar em $reconnect_duration_in_seconds$ segundos", "submitDebugLog": "Registro de depuração ", "debugLog": "Registro de depuração ", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Ir para Notas de Lançamento", "goToSupportPage": "Ir para Página de Suporte", "menuReportIssue": "Relatar um problema", @@ -109,13 +110,14 @@ "continue": "Continuar", "error": "Erro", "delete": "Excluir", - "deletePublicWarning": "Você tem certeza? Isso irá remover permanentemente essa mensagem para todos nesse grupo aberto.", - "deleteMultiplePublicWarning": "Você tem certeza? Isso irá remover permanentemente essas mensagens para todos nesse grupo aberto.", - "deleteWarning": "Tem certeza? Ao clicar em 'Excluir', você remove esta mensagem permanentemente deste dispositivo apenas.", - "deleteMultipleWarning": "Você tem certeza? Clicar em 'excluir' irá remover permanentemente essas mensagens apenas desse dispositivo.", "messageDeletionForbidden": "Você não tem permissão para excluír as mensagens de outros", - "deleteThisMessage": "Excluir mensagem", + "deleteJustForMe": "Excluir apenas para mim", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Apagar mensagens", "deleted": "Excluído", + "messageDeletedPlaceholder": "Esta mensagem foi excluída", "from": "De:", "to": "Para:", "sent": "Enviada", @@ -124,17 +126,9 @@ "groupMembers": "Membros do grupo", "moreInformation": "Mais informações", "resend": "Reenviar", - "deleteMessage": "Excluir Mensagem", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Apagar mensagens", - "deleteMessageForEveryone": "Excluír mensagem para todos", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Excluír mensagens para todos", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Você deseja apagar esta conversa definitivamente?", "clearAllData": "Limpar todos os dados", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Isso excluirá permanentemente suas mensagens, sessões e contatos.", "deleteContactConfirmation": "Tem certeza de que deseja excluir esta conversa?", "quoteThumbnailAlt": "Miniatura da imagem na citação", "imageAttachmentAlt": "Imagem anexa à mensagem", @@ -146,6 +140,7 @@ "copySessionID": "Copiar ID Session", "copyOpenGroupURL": "Copiar URL do grupo", "save": "Salvar", + "saveLogToDesktop": "Save log to desktop", "saved": "Salvo", "permissions": "Permissões", "general": "Geral", @@ -153,9 +148,9 @@ "savedTheFile": "Mídia salva por $name$", "linkPreviewsTitle": "Enviar Pré-Visualizações De Links", "linkPreviewDescription": "Previews are supported for most urls.", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microfone e Câmera", - "mediaPermissionsDescription": "Permitir acesso à câmera e ao microfone", + "linkPreviewsConfirmMessage": "Você não terá proteção completa de metadados ao receber pré-visualização de links.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Corretor ortográfico", "spellCheckDescription": "Ativar a verificação ortográfica do texto digitado na caixa de edição de mensagens", "spellCheckDirty": "Você precisa reiniciar o Session para aplicar as novas configurações", @@ -259,7 +254,7 @@ "banUser": "Banir usuário", "banUserConfirm": "Are you sure you want to ban user?", "banUserAndDeleteAll": "Banir e Excluir Tudo", - "banUserAndDeleteAllConfirm": "Tu tens certeza de que queres banir o usuário e apagar todas as suas mensagens?", + "banUserAndDeleteAllConfirm": "Tem certeza de que deseja bloquear o usuário e excluir todas suas mensagens?", "userBanned": "User banned successfully", "userBanFailed": "Banimento falhou!", "leaveGroup": "Sair Do Grupo", @@ -421,6 +416,7 @@ "unpinConversation": "Desafixar conversa", "pinConversationLimitTitle": "Limite de conversas fixadas", "pinConversationLimitToastDescription": "Você só pode fixar $number$ conversas", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "A primeira mensagem não lida está acima", "sendRecoveryPhraseTitle": "Enviando frase de recuperação", "sendRecoveryPhraseMessage": "Tu estás tentando enviar a tua frase de recuperação qual pode ser utilizada para acessar a tua conta. Tu tens certeza de que queres enviar esta mensagem?", @@ -431,30 +427,40 @@ "dialogClearAllDataDeletionQuestion": "Tu gostarias de limpar apenas este dispositivo, ou excluir toda a sua conta?", "deviceOnly": "Só Dispositivo", "entireAccount": "Toda a Conta", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "areYouSureDeleteDeviceOnly": "Você tem certeza que deseja excluir os dados apenas do seu dispositivo?", + "areYouSureDeleteEntireAccount": "Tem certeza de que deseja excluir a sua conta completamente, incluindo os dados de rede?", + "iAmSure": "Tenho certeza", "recoveryPhraseSecureTitle": "Tu estás quase acabando!", "recoveryPhraseRevealMessage": "Proteja a sua conta salvando a sua frase de recuperação. Revele a sua frase de recuperação e então armazene-a com segurança para protegê-la.", "recoveryPhraseRevealButtonText": "Revelar Frase de Recuperação", "notificationSubtitle": "Notificações - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Responda à pesquisa do Session", + "goToOurSurvey": "Acessar nossa pesquisa", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Aceitar", + "decline": "Rejeitar", + "endCall": "Encerrando chamada", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Não foi possível iniciar nova chamada", + "callMissed": "Chamada perdida de $name$", + "callMissedTitle": "Chamada perdida", + "noCameraFound": "Nenhuma câmera encontrada", + "noAudioInputFound": "Nenhuma entrada de áudio encontrada", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index 26d740e6a..6f2ede528 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Nova tentativa de ligação dentro de $reconnect_duration_in_seconds$ segundos", "submitDebugLog": "Enviar relatório de depuração", "debugLog": "Relatório de depuração", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Ir para as notas de lançamento", "goToSupportPage": "Ir para a página de suporte", "menuReportIssue": "Reportar um problema", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Erro", "delete": "Eliminar", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Tem a certeza? Clicar em 'Eliminar' irá remover permanentemente esta mensagem deste dispositivo.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Eliminar esta mensagem", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Eliminar mensagens", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "De", "to": "para", "sent": "Enviada", @@ -124,14 +126,6 @@ "groupMembers": "Membros do grupo", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Eliminar mensagem", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Eliminar mensagens", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Deseja eliminar definitivamente esta conversa?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Guardar", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissões", "general": "Geral", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Permitir o acesso à câmara e ao microfone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Ative a verificação ortográfica para o texto introduzido", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index d70e7cf2c..38330ab7e 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Se încearcă reconectarea în $reconnect_duration_in_seconds$ secunde", "submitDebugLog": "Jurnal depanare", "debugLog": "Jurnalul de depanare", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Mergi la Notele de Lansare", "goToSupportPage": "Mergi la Pagina de Suport", "menuReportIssue": "Raportează o problemă", @@ -109,13 +110,14 @@ "continue": "Continuă", "error": "Eroare", "delete": "Șterge", - "deletePublicWarning": "Ești sigur? Acest lucru va elimina definitiv acest mesaj pentru toți cei din acest grup deschis.", - "deleteMultiplePublicWarning": "Ești sigur? Acest lucru va șterge definitiv aceste mesaje pentru toți cei din acest grup deschis.", - "deleteWarning": "Ești sigur? Apăsând 'ștergere' vei șterge permanent acest mesaj doar de pe acest dispozitiv.", - "deleteMultipleWarning": "Ești sigur? Apăsând 'ștergere' vei șterge permanent acest mesaj doar de pe acest dispozitiv.", "messageDeletionForbidden": "Nu aveţi permisiunea de a şterge mesajele altora", - "deleteThisMessage": "Şterge acest mesaj", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Șterge mesajele", "deleted": "Șters", + "messageDeletedPlaceholder": "This message has been deleted", "from": "De la", "to": "către", "sent": "Trimis", @@ -124,14 +126,6 @@ "groupMembers": "Membrii grupului", "moreInformation": "Mai multe informații", "resend": "Retrimite", - "deleteMessage": "Şterge mesajul", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Șterge mesajele", - "deleteMessageForEveryone": "Ştergeţi mesajul pentru toată lumea", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Ştergeţi mesajele pentru toată lumea", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Şterg permanent acestă conversație?", "clearAllData": "Șterge toate datele", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copiază ID-ul Session", "copyOpenGroupURL": "Copiază URL-ul grupului", "save": "Salvează", + "saveLogToDesktop": "Save log to desktop", "saved": "Salvat", "permissions": "Permisiuni", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Trimite Previzualizări Link", "linkPreviewDescription": "Previzualizările sunt suportate pentru majoritatea adreselor URL", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microfon și Cameră", - "mediaPermissionsDescription": "Permite accesul la cameră și microfon", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Verificare ortografie", "spellCheckDescription": "Activează verificarea ortografică a textului introdus în caseta de compoziție a mesajului", "spellCheckDirty": "Trebuie să reporniți Session pentru a aplica noile setări", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 9b96e6371..472455899 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -19,11 +19,11 @@ "editMenuPaste": "Вставить", "editMenuPasteAndMatchStyle": "Вставить с соответствием стилю", "editMenuDelete": "Удалить", - "editMenuSelectAll": "Выбрать всё", + "editMenuSelectAll": "Выбрать все", "windowMenuClose": "Закрыть окно", "windowMenuMinimize": "Свернуть", "windowMenuZoom": "Увеличить", - "windowMenuBringAllToFront": "Вынести всё на передний план", + "windowMenuBringAllToFront": "Все на передний план", "viewMenuResetZoom": "Фактический размер", "viewMenuZoomIn": "Увеличить", "viewMenuZoomOut": "Уменьшить", @@ -48,7 +48,7 @@ "reportIssue": "Сообщить о проблеме", "gotIt": "Понятно", "submit": "Отправить", - "markAllAsRead": "Отметить всё как прочитанное", + "markAllAsRead": "Отметить все как прочитанное", "incomingError": "Ошибка при обработке входящего сообщения", "media": "Медиа", "mediaEmptyState": "Нет медиа-файлов", @@ -73,6 +73,7 @@ "attemptingReconnection": "Попытка переподключения через $reconnect_duration_in_seconds$ секунд", "submitDebugLog": "Журнал отладки", "debugLog": "Журнал отладки", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Перейти к заметкам о релизе", "goToSupportPage": "Перейти на страницу поддержки", "menuReportIssue": "Сообщить о проблеме", @@ -101,7 +102,7 @@ "audio": "Аудио", "video": "Видео", "photo": "Фото", - "cannotUpdate": "Не удаётся обновить", + "cannotUpdate": "Обновить не удается", "cannotUpdateDetail": "Не удалось обновить Session Desktop, но доступна новая версия. Пожалуйста, перейдите на https://getsession.org/ и установите новую версию вручную, затем свяжитесь со службой поддержки или создайте сообщение об этой ошибке.", "ok": "ОК", "cancel": "Отменить", @@ -109,13 +110,14 @@ "continue": "Продолжить", "error": "Ошибка", "delete": "Удалить", - "deletePublicWarning": "Вы уверены? Это навсегда удалит данное сообщение для всех участников этой открытой группы.", - "deleteMultiplePublicWarning": "Вы уверены? Это навсегда удалит данные сообщения для всех участников этой открытой группы.", - "deleteWarning": "Вы уверены? Нажав 'удалить' вы навсегда удалите данное сообщение только с этого устройства.", - "deleteMultipleWarning": "Вы уверены? Нажав 'удалить', вы навсегда удалите данные сообщения только с этого устройства.", "messageDeletionForbidden": "У вас недостаточно прав для удаления чужих сообщений", - "deleteThisMessage": "Удалить сообщение", + "deleteJustForMe": "Удалить только у меня", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Удалить сообщения", "deleted": "Удалено", + "messageDeletedPlaceholder": "Это сообщение было удалено", "from": "От:", "to": "Кому:", "sent": "Отправлено", @@ -123,18 +125,10 @@ "sendMessage": "Отправить сообщение", "groupMembers": "Участники группы", "moreInformation": "Больше информации", - "resend": "Отправить ещё раз", - "deleteMessage": "Удалить сообщение", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Удалить сообщения", - "deleteMessageForEveryone": "Удалить сообщение для всех", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Удалить сообщения для всех", - "deleteForEveryone": "Удалить для всех", + "resend": "Отправить повторно", "deleteConversationConfirmation": "Удалить эту беседу без возможности восстановления?", "clearAllData": "Очистить все данные", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "Это навсегда удалит ваши сообщения и контакты.", "deleteContactConfirmation": "Вы уверены, что хотите удалить эту беседу?", "quoteThumbnailAlt": "Миниатюра изображения из цитируемого сообщения", "imageAttachmentAlt": "Изображение, прикрепленное к сообщению", @@ -146,6 +140,7 @@ "copySessionID": "Копировать Session ID", "copyOpenGroupURL": "Копировать URL группы", "save": "Сохранить", + "saveLogToDesktop": "Save log to desktop", "saved": "Сохранено", "permissions": "Разрешения", "general": "Общие", @@ -153,9 +148,9 @@ "savedTheFile": "$name$ сохранил медиафайл", "linkPreviewsTitle": "Отправлять предпросмотр ссылки", "linkPreviewDescription": "Предпросмотр доступен для большинства URL", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Микрофон и камера", - "mediaPermissionsDescription": "Разрешить доступ к камере и микрофону", + "linkPreviewsConfirmMessage": "У вас не будет полной защиты метаданных при отправке предварительного просмотра ссылок.", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Проверка орфографии", "spellCheckDescription": "Включить проверку орфографии текста, введенного в поле создания сообщения", "spellCheckDirty": "Вы должны перезапустить Session, чтобы применить новые настройки", @@ -258,7 +253,7 @@ "userUnbanFailed": "Не удалось разблокировать!", "banUser": "Заблокировать пользователя", "banUserConfirm": "Вы уверены, что хотите заблокировать пользователя?", - "banUserAndDeleteAll": "Заблокировать и удалить всё", + "banUserAndDeleteAll": "Заблокировать и удалить все", "banUserAndDeleteAllConfirm": "Вы уверены, что хотите заблокировать пользователя и удалить все его сообщения?", "userBanned": "Пользователь заблокирован", "userBanFailed": "Не удалось заблокировать!", @@ -288,7 +283,7 @@ "confirmPassword": "Подтвердить пароль", "pasteLongPasswordToastTitle": "Содержимое буфера обмена превышает максимальную длину пароля в $max_pwd_len$ символов.", "showRecoveryPhrasePasswordRequest": "Пожалуйста, введите ваш пароль", - "recoveryPhraseSavePromptMain": "Ваша секретная фраза является главным ключом к вашему Session ID. Вы можете использовать ее для восстановления Session ID, если потеряете доступ к своему устройству. Сохраните свою секретную фразу в безопасном месте, и никому её не передавайте.", + "recoveryPhraseSavePromptMain": "Фраза восстановления является главным ключом к Session ID — вы можете использовать ее для восстановления, если потеряете доступ к своему устройству. Храните вашу фразу восстановления в надежном месте и никому ее не передавайте.", "invalidOpenGroupUrl": "Недействительный URL-адрес", "copiedToClipboard": "Скопировано в буфер обмена", "passwordViewTitle": "Введите ваш пароль", @@ -304,11 +299,11 @@ "noGivenPassword": "Пожалуйста, введите ваш пароль", "passwordsDoNotMatch": "Пароли не совпадают", "setPasswordInvalid": "Пароли не совпадают", - "changePasswordInvalid": "Старый введённый пароль неверен", + "changePasswordInvalid": "Старый пароль, который вы ввели, неверен", "removePasswordInvalid": "Неверный пароль", "setPasswordTitle": "Установить пароль", - "changePasswordTitle": "Изменённый пароль", - "removePasswordTitle": "Удалённый пароль", + "changePasswordTitle": "Пароль изменен", + "removePasswordTitle": "Пароль удален", "setPasswordToastDescription": "Ваш пароль установлен. Пожалуйста, храните его в безопасном месте.", "changePasswordToastDescription": "Ваш пароль был изменен. Пожалуйста, храните его в безопасном месте.", "removePasswordToastDescription": "Вы удалили ваш пароль.", @@ -387,7 +382,7 @@ "closedGroupMaxSize": "В закрытой группе не может быть больше 100 участников", "noBlockedContacts": "Нет заблокированных контактов", "userAddedToModerators": "Пользователь добавлен в список модераторов", - "userRemovedFromModerators": "Пользователь удалён из списка модераторов", + "userRemovedFromModerators": "Пользователь удален из списка модераторов", "orJoinOneOfThese": "Или присоединитесь к одной из этих...", "helpUsTranslateSession": "Помогите нам перевести Session", "translation": "Перевод", @@ -421,6 +416,7 @@ "unpinConversation": "Открепить беседу", "pinConversationLimitTitle": "Лимит закрепленных бесед", "pinConversationLimitToastDescription": "Вы можете закрепить только $number$ бесед", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Первое непрочитанное сообщение находится выше", "sendRecoveryPhraseTitle": "Отправка секретной фразы", "sendRecoveryPhraseMessage": "Вы пытаетесь отправить вашу секретную фразу, которая может быть использована для доступа к вашей учетной записи. Вы уверены, что хотите отправить это сообщение?", @@ -431,30 +427,40 @@ "dialogClearAllDataDeletionQuestion": "Вы хотите очистить только это устройство или полностью удалить ваш аккаунт?", "deviceOnly": "Только устройство", "entireAccount": "Полностью аккаунт", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", + "areYouSureDeleteDeviceOnly": "Вы уверены, что хотите удалить только данные вашего устройства?", + "areYouSureDeleteEntireAccount": "Вы уверены, что хотите удалить всю свою учетную запись, включая сетевые данные?", + "iAmSure": "Я уверен", "recoveryPhraseSecureTitle": "Вы почти закончили!", "recoveryPhraseRevealMessage": "Обезопасьте свой аккаунт, сохранив вашу секретную фразу. Посмотрите вашу секретную фразу, затем сохраните ее в безопасном месте.", "recoveryPhraseRevealButtonText": "Показать секретную фразу", "notificationSubtitle": "Уведомления - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "surveyTitle": "Пройдите наш опрос о Session", + "goToOurSurvey": "Перейти к опросу", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "Принять", + "decline": "Отклонить", + "endCall": "Завершить вызов", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "Не удается начать новый вызов", + "callMissed": "Пропущенный вызов от $name$", + "callMissedTitle": "Пропущен вызов", + "noCameraFound": "Камера не найдена", + "noAudioInputFound": "Аудиовход не найден", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/si/messages.json b/_locales/si/messages.json index 59a6fcba3..e40b3c618 100644 --- a/_locales/si/messages.json +++ b/_locales/si/messages.json @@ -1,7 +1,7 @@ { "privacyPolicy": "නියම සහ රහස්‍යතා ප්‍රතිපත්තිය", "copyErrorAndQuit": "දෝෂය පිටපත් කර ඉවත්වන්න", - "unknown": "Unknown", + "unknown": "නොදන්නා", "databaseError": "දත්තසමුදායේ දෝෂයකි", "mainMenuFile": "ගොනුව (&F)", "mainMenuEdit": "සංස්කරණය (&E)", @@ -9,7 +9,7 @@ "mainMenuWindow": "කවුළුව (&W)", "mainMenuHelp": "උදව් (&H)", "appMenuHide": "සඟවන්න", - "appMenuHideOthers": "Hide Others", + "appMenuHideOthers": "අන් අය සඟවන්න", "appMenuUnhide": "සියල්ල පෙන්වන්න", "appMenuQuit": "සෙෂන් වෙතින් ඉවත්වන්න", "editMenuUndo": "පෙරසේ", @@ -21,7 +21,7 @@ "editMenuDelete": "Delete", "editMenuSelectAll": "Select all", "windowMenuClose": "කවුළුව වසන්න", - "windowMenuMinimize": "Minimize", + "windowMenuMinimize": "හකුළන්න", "windowMenuZoom": "විශාලනය", "windowMenuBringAllToFront": "Bring All to Front", "viewMenuResetZoom": "සැබෑ ප්‍රමාණය", @@ -30,17 +30,17 @@ "viewMenuToggleFullScreen": "Toggle Full Screen", "viewMenuToggleDevTools": "Toggle Developer Tools", "contextMenuNoSuggestions": "යෝජනා නැත", - "openGroupInvitation": "Open group invitation", + "openGroupInvitation": "සමූහ ඇරයුම විවෘතකරන්න", "joinOpenGroupAfterInvitationConfirmationTitle": "$roomName$ ට එක්වනවාද?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", + "joinOpenGroupAfterInvitationConfirmationDesc": "ඔබට $roomName$ විවෘත සමූහයට එක්වීමට අවශ්‍ය බව විශ්වාසද?", "enterSessionIDOrONSName": "Enter Session ID or ONS name", "loading": "පූරණය වෙමින්…", "optimizingApplication": "Optimizing application...", "done": "Done", "me": "මා", "view": "View", - "youLeftTheGroup": "You have left the group.", - "youGotKickedFromGroup": "You were removed from the group.", + "youLeftTheGroup": "ඔබ සමූහය හැර ගොස් ඇත.", + "youGotKickedFromGroup": "ඔබව සමූහයෙන් ඉවත් කර ඇත.", "unreadMessage": "නොකියවූ පණිවිඩය", "unreadMessages": "නොකියවූ පණිවිඩ", "debugLogExplanation": "This log will be posted publicly online for contributors to view. You may examine and edit it before submitting.", @@ -48,7 +48,7 @@ "reportIssue": "Report an issue", "gotIt": "Got it", "submit": "යොමන්න", - "markAllAsRead": "Mark All as Read", + "markAllAsRead": "සියල්ල කියවූ ලෙස සලකුණු කරන්න", "incomingError": "Error handling incoming message", "media": "මාධ්‍යය", "mediaEmptyState": "මාධ්‍යය නැත", @@ -59,7 +59,7 @@ "thisWeek": "මෙම සතිය", "thisMonth": "මෙම මාසය", "voiceMessage": "හඬ පණිවිඩය", - "dangerousFileType": "For security reasons, this file type cannot be sent", + "dangerousFileType": "ආරක්‍ෂක හේතූන් මත මෙම ගොනු වර්ගය යැවීමට නොහැකිය", "stagedPreviewThumbnail": "Draft thumbnail link preview for $domain$", "previewThumbnail": "Thumbnail link preview for $domain$", "stagedImageAttachment": "Draft image attachment: $path$", @@ -73,11 +73,12 @@ "attemptingReconnection": "තත්. $reconnect_duration_in_seconds$ න් නැවත සම්බන්ධවීමට තැත් කරනු ඇත", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "නිකුතු සටහන් වෙත යන්න", "goToSupportPage": "සහාය පිටුවට යන්න", "menuReportIssue": "Report an Issue", "about": "පිළිබඳව", - "speech": "Speech", + "speech": "කතා කරන්න", "show": "පෙන්වන්න", "sessionMessenger": "සෙෂන්", "search": "සොයන්න", @@ -109,13 +110,14 @@ "continue": "ඉදිරියට", "error": "දෝෂය", "delete": "Delete", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Delete Messages", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "වෙතින්:", "to": "වෙත:", "sent": "යැවිණි", @@ -124,14 +126,6 @@ "groupMembers": "{group} සාමාජිකයින්", "moreInformation": "තව තොරතුරු", "resend": "නැවත යවන්න", - "deleteMessage": "Delete Message", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Delete Messages", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete for Everyone", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "සමූහයේ ඒ.ස.නි. පිටපත්කරන්න", "save": "සුරකින්න", + "saveLogToDesktop": "Save log to desktop", "saved": "සුරැකිණි", "permissions": "අවසර", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "සබැඳියේ පෙරදසුන් යවන්න", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "නොකියවූ පළමු පණිවිඩය ඉහත ඇත", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "දැනුම්දීම් - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/sk/messages.json b/_locales/sk/messages.json index 402293173..d91d1ba59 100644 --- a/_locales/sk/messages.json +++ b/_locales/sk/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Pokus o znovupripojenie za $reconnect_duration_in_seconds$ sekúnd", "submitDebugLog": "Ladiaci log", "debugLog": "Ladiaci Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Navštíviť Poznámky k Vydaniu", "goToSupportPage": "Navštíviť Stránku Podpory", "menuReportIssue": "Nahlásiť Problém", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Chyba", "delete": "Vymazať", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Ste si istý? Kliknutím na \"Zmazať\" natrvalo odstránite túto správu iba z tohto zariadenia.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Vymazať túto správu", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Zmazať správy", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Od", "to": "pre", "sent": "Odoslaná", @@ -124,14 +126,6 @@ "groupMembers": "Členy skupiny", "moreInformation": "More information", "resend": "Znovu odoslať", - "deleteMessage": "Vymazať správu", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Zmazať správy", - "deleteMessageForEveryone": "Zmazať správu pre každého", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Zmazať správy pre každého", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Natrvalo zmazať túto konverzáciu?", "clearAllData": "Odstrániť všetky dáta", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Kopírovať Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Uložiť", + "saveLogToDesktop": "Save log to desktop", "saved": "Uložené", "permissions": "Povolenia", "general": "Všeobecné", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Povoliť prústup ku kamere a mikrofónu", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Povoliť kontrolu pravopisu textu zadaného do textového poľa", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/sl/messages.json b/_locales/sl/messages.json index 2e4ad77ed..14ea6c2e0 100644 --- a/_locales/sl/messages.json +++ b/_locales/sl/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Naslednji poskus povezave: $reconnect_duration_in_seconds$ s", "submitDebugLog": "Sistemska zabeležba", "debugLog": "Sistemska zabeležba", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Opombe k izdaji", "goToSupportPage": "Podporna stran", "menuReportIssue": "Prijava napake", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Napaka", "delete": "Izbriši", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Ste prepričani? S klikom na 'izbriši' boste nepovratno izbrisali sporočilo s te naprave.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Izbriši to sporočilo", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Izbriši sporočila", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Pošiljatelj", "to": "v", "sent": "Poslano", @@ -124,14 +126,6 @@ "groupMembers": "Člani skupine", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Izbriši sporočilo", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Izbriši sporočila", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Ali res želite nepovratno izbrisati ta pogovor?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Shrani", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Dovoljenja", "general": "Splošno", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Dovoli dostop do kamere in mikrofona", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Vklop črkovalnika v okencu za vnašanje sporočila", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/sq/messages.json b/_locales/sq/messages.json index 755a93d9a..9c590c281 100644 --- a/_locales/sq/messages.json +++ b/_locales/sq/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Përpjekje për rilidhje pas $reconnect_duration_in_seconds$ sekondash", "submitDebugLog": "Regjistër diagnostikimesh", "debugLog": "Regjistër Diagnostikimesh", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Kalo te Shënime Versioni", "goToSupportPage": "Kalo te Faqja e Asistencës", "menuReportIssue": "Njoftoni një Problem", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Gabim", "delete": "Fshije", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Jeni i sigurt? Klikimi mbi 'fshije' do të fshijë përgjithmonë këtë mesazh vetëm nga kjo pajisje.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Fshije këtë mesazh", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Fshini mesazhe", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Nga", "to": "për", "sent": "Dërguar më", @@ -124,14 +126,6 @@ "groupMembers": "Anëtarë grupi", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Fshije Mesazhin", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Fshini mesazhe", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Të fshihet përgjithmonë kjo bisedë?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Ruaje", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Leje", "general": "Të përgjithshme", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Lejo përdorim të kamerës dhe mikrofonit", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Aktivizo kontroll drejtshkrimi të tekstit të dhënë te kutia e hartimit të mesazheve", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 6329524b1..a623c71a3 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Поновно повезивање за $reconnect_duration_in_seconds$ секунде/и", "submitDebugLog": "Извештај о грешкама", "debugLog": "Izveštaj o greškama", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Idite na napomene o verziji", "goToSupportPage": "Idite na stranicu za podršku", "menuReportIssue": "Prijavi problem", @@ -109,13 +110,14 @@ "continue": "Nastavi", "error": "Грешка", "delete": "Обриши", - "deletePublicWarning": "Jesi li siguran? Ovo će trajno ukloniti ovu poruku za sve u ovoj otvorenoj grupi.", - "deleteMultiplePublicWarning": "Jesi li siguran? Ovo će trajno ukloniti ovu poruku za sve u ovoj otvorenoj grupi.", - "deleteWarning": "Jesi li sigurni? Klikom na „obriši“ ovu poruku ćete trajno ukloniti samo sa ovog uređaja.", - "deleteMultipleWarning": "Jesi li sigurni? Klikom na „obriši“ ovu poruku ćete trajno ukloniti samo sa ovog uređaja.", "messageDeletionForbidden": "Nemate dozvolu da brišete tuđe poruke", - "deleteThisMessage": "Обриши ову поруку", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Уклони пошиљке", "deleted": "Obrisano", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Од", "to": "to", "sent": "Послата", @@ -124,14 +126,6 @@ "groupMembers": "Чланови групе", "moreInformation": "Više informacija", "resend": "Pošalji ponovo", - "deleteMessage": "Obriši poruku", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Уклони пошиљке", - "deleteMessageForEveryone": "Obriši poruku za sve", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Obriši poruku za sve", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Неопозиво уклонити преписку?", "clearAllData": "Počisti sve podatke", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Kreiraj Session ID", "copyOpenGroupURL": "Kopiraj URL-ove grupa", "save": "Сачувај", + "saveLogToDesktop": "Save log to desktop", "saved": "Sačuvano", "permissions": "Dozvole", "general": "Опште", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Pošalji preglede veza", "linkPreviewDescription": "Pregledi su podržani za većinu URL-ova", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Mikrofon i kamera", - "mediaPermissionsDescription": "Dozvoli pristup kameri i mikrofonu", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Provеra pravopisa", "spellCheckDescription": "Omogući proveru pravopisa teksta", "spellCheckDirty": "Neophodno je da ponovo pokrenute sesiju da biste primenili nova podešavanja", @@ -421,6 +416,7 @@ "unpinConversation": "Otkači konverzaciju sa vrha", "pinConversationLimitTitle": "Limit zalepljenih konverzacija", "pinConversationLimitToastDescription": "Možete zalepiti najviše $number$ konverzacija", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "Prva nepročitana poruka je gore", "sendRecoveryPhraseTitle": "Slanje fraze za oporavak", "sendRecoveryPhraseMessage": "Pokušavate da pošaljete svoju frazu za oporavak koja se može koristiti za pristup vašem nalogu. Jeste li sigurni da želite poslati ovu poruku?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Zaštitite svoj nalog tako što ćete sačuvati frazu za oporavak. Pogledajte svoju frazu za oporavak, a zatim je bezbedno sačuvajte.", "recoveryPhraseRevealButtonText": "Slanje fraze za oporavak", "notificationSubtitle": "Notifikacije - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 79e3ff2fc..cb0929c24 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Försöker återansluta om $reconnect_duration_in_seconds$ sekunder", "submitDebugLog": "Loggfil för felsökning", "debugLog": "Felsökningslogg", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Gå till versionsanteckningar", "goToSupportPage": "Gå till supportsidan", "menuReportIssue": "Rapportera ett fel", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "Fel", "delete": "Radera", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Är du säker? Om du klickar 'radera' så tas detta meddelande bort permanent, men bara från denna enhet.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Radera detta meddelande", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Radera meddelanden", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Från", "to": "till", "sent": "Skickat", @@ -124,14 +126,6 @@ "groupMembers": "Gruppmedlemmar", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "Radera meddelande", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Radera meddelanden", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Vill du radera denna konversation för alltid?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Spara", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Behörigheter", "general": "Generell", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Tillåt åtkomst till kameran och mikrofonen", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Slå på stavningskontroll för text som anges i meddelandefältet", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/ta/messages.json b/_locales/ta/messages.json index 41bd1811e..deb903e3f 100644 --- a/_locales/ta/messages.json +++ b/_locales/ta/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "தொடரு", "error": "கோலாரு", "delete": "நீக்கு", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Delete message", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "தகவலைகலை நீக்கு", "deleted": "நீக்கபடுல்லது", + "messageDeletedPlaceholder": "This message has been deleted", "from": "அனுப்புனர்:", "to": "பெறுநர்:", "sent": "அனுப்பப்பட்டது", @@ -124,14 +126,6 @@ "groupMembers": "குழு ", "moreInformation": "மேற்கொண்ட தகவல்கள்", "resend": "மீண்டும் அனுப்பு", - "deleteMessage": "தகவலை நீக்கு", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "தகவலைகலை நீக்கு", - "deleteMessageForEveryone": "எல்லோருக்கும் தகவலை நீக்கு", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "எல்லோருக்கும் தகவலைகளை நீக்கு", - "deleteForEveryone": "எல்லோருக்கும் தகவலைகளை நீக்கு", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Save", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/th/messages.json b/_locales/th/messages.json index f2c68f81c..958f7b989 100644 --- a/_locales/th/messages.json +++ b/_locales/th/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "กำลังพยายามเชื่อมต่อใหม่อีกครั้งในอีก $reconnect_duration_in_seconds$ วินาที", "submitDebugLog": "บันทึกดีบัก", "debugLog": "บันทึกดีบัก", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "ไปที่บันทึกประจำรุ่น", "goToSupportPage": "ไปที่หน้าการสนับสนุน", "menuReportIssue": "รายงานปัญหา", @@ -109,13 +110,14 @@ "continue": "Continue", "error": "ข้อผิดพลาด", "delete": "ลบ", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "คุณแน่ใจหรือ การคลิกที่ 'ลบ' จะเป็นการลบข้อความนี้ออกไปจากอุปกรณ์นี้อย่างถาวร", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "ลบข้อความนี้", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "ลบข้อความ", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "จาก", "to": "to", "sent": "ส่งแล้ว", @@ -124,14 +126,6 @@ "groupMembers": "สมาชิกกลุ่ม", "moreInformation": "More information", "resend": "Resend", - "deleteMessage": "ลบข้อความนี้", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "ลบข้อความ", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "ลบการสนทนานี้โดยถาวรหรือไม่", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "บันทึก", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "สิทธิ์", "general": "General", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "อนุญาตให้เข้าถึงกล้องและไมโครโฟน", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index a411c9b44..aaeb9f99a 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -1,7 +1,7 @@ { - "privacyPolicy": "Terms & Privacy Policy", + "privacyPolicy": "Şartlar ve Gizlilik Politikası", "copyErrorAndQuit": "Hatayı kopyala ve çık", - "unknown": "Unknown", + "unknown": "Bilinmeyen", "databaseError": "Veritabanı hatası", "mainMenuFile": "&Dosya", "mainMenuEdit": "&Düzenle", @@ -29,26 +29,26 @@ "viewMenuZoomOut": "Uzaklaştır", "viewMenuToggleFullScreen": "Tam Ekranı Aç/Kapat", "viewMenuToggleDevTools": "Geliştirici Araçlarını Aç/Kapat", - "contextMenuNoSuggestions": "No Suggestions", - "openGroupInvitation": "Open group invitation", - "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", - "enterSessionIDOrONSName": "Enter Session ID or ONS name", + "contextMenuNoSuggestions": "Öneri Yok", + "openGroupInvitation": "Açık grup daveti", + "joinOpenGroupAfterInvitationConfirmationTitle": "$roomName$'e katıl?", + "joinOpenGroupAfterInvitationConfirmationDesc": "$roomName$ açık gurubuna katılmak istediğinize emin misiniz?", + "enterSessionIDOrONSName": "Session Kimliğini veya ONS adını girin", "loading": "Yükleniyor...", "optimizingApplication": "Uygulama optimize ediliyor...", - "done": "Done", + "done": "Tamamlandı", "me": "Ben", "view": "Görüntüle", "youLeftTheGroup": "Gruptan ayrıldınız", - "youGotKickedFromGroup": "You were removed from the group.", - "unreadMessage": "Unread Message", - "unreadMessages": "Unread Messages", + "youGotKickedFromGroup": "Gruptan atıldınız.", + "unreadMessage": "Okunmamış İletiler", + "unreadMessages": "Okunmamış İleti", "debugLogExplanation": "Bu günlük katkıda bulunanların görebilmeleri için herkese açık bir şekilde gönderilecektir. Göndermeden önce inceleyip düzenleyebilirsiniz.", "debugLogError": "Yüklemede bir şeyler ters gitti! Lütfen günlüğü hazırlayacağınız hata kaydına kendiniz ekleyin.", "reportIssue": "Sorun bildir", "gotIt": "Anladım!", "submit": "Gönder", - "markAllAsRead": "Mark All as Read", + "markAllAsRead": "Tümünü Okundu Olarak İşaretle", "incomingError": "Gelen ileti işlenirken hata oluştu", "media": "Medya", "mediaEmptyState": "Bu sohbette hiç medya yok", @@ -73,10 +73,11 @@ "attemptingReconnection": "Yeniden bağlantı kurma $reconnect_duration_in_seconds$ saniye içerisinde tekrar denenecektir.", "submitDebugLog": "Hata ayıklama günlüğü", "debugLog": "Hata Ayıklama Günlüğü", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Sürüm Notlarına Git", "goToSupportPage": "Destek Sayfasına Git", "menuReportIssue": "Sorun Bildir", - "about": "About", + "about": "Hakkında", "speech": "Konuşma", "show": "Göster", "sessionMessenger": "oturum", @@ -90,7 +91,7 @@ "contactAvatarAlt": "$name$ kişisinin avatarı", "downloadAttachment": "Eklentiyi İndir", "replyToMessage": "İletiyi Yanıtla", - "replyingToMessage": "Replying to:", + "replyingToMessage": "Adlı kişiye yanıt olarak:", "originalMessageNotFound": "İletinin aslı bulunamadı", "originalMessageNotAvailable": "İletinin aslı artık mevcut değil", "messageFoundButNotLoaded": "İletinin aslı bulundu, ama yüklenmedi. Yüklemek için yukarıya kaydırın.", @@ -101,37 +102,30 @@ "audio": "Ses", "video": "Video", "photo": "Fotoğraf", - "cannotUpdate": "Cannot Update", + "cannotUpdate": "Güncellenemiyor", "cannotUpdateDetail": "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", "ok": "TAMAM", "cancel": "İptal", - "close": "Close", + "close": "Kapat", "continue": "Devam Et", "error": "Hata", "delete": "Sil", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Emin misiniz? 'Sil'e tıklamak bu iletiyi sadece bu cihazdan kalıcı olarak silecektir.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Bu iletiyi sil", - "deleted": "Deleted", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "İletileri sil", + "deleted": "Silindi", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Gönderen", "to": "alıcı", "sent": "Gönderildi", "received": "Alındı", "sendMessage": "İleti gönder", "groupMembers": "Grup üyeleri", - "moreInformation": "More information", - "resend": "Resend", - "deleteMessage": "İletiyi Sil", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "İletileri sil", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", + "moreInformation": "Daha fazla bilgi", + "resend": "Tekrar gönder", "deleteConversationConfirmation": "Bu sohbeti kalıcı olarak sil?", "clearAllData": "Clear All Data", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -142,10 +136,11 @@ "lightboxImageAlt": "Konuşmada gönderilen görüntü", "imageCaptionIconAlt": "Bu görüntünün başlığı olduğunu gösteren ikon", "addACaption": "Bir başlık ekleyin...", - "copy": "Copy", - "copySessionID": "Copy Session ID", + "copy": "Kopyala", + "copySessionID": "Session Kimliğini Kopyala", "copyOpenGroupURL": "Copy Group's URL", "save": "Kaydet", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "İzinler", "general": "Genel", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Kamera ve mikrofona erişim izni ver", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "İleti kutusuna girilen sözcüklerin denetlenmesini etkinleştir", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -247,15 +242,15 @@ "multipleJoinedTheGroup": "$names$ gruba katıldı", "kickedFromTheGroup": "$name$ was removed from the group.", "multipleKickedFromTheGroup": "$name$ were removed from the group.", - "blockUser": "Block", - "unblockUser": "Unblock", - "unblocked": "Unblocked", - "blocked": "Blocked", - "blockedSettingsTitle": "Blocked contacts", - "unbanUser": "Unban User", + "blockUser": "Engelle", + "unblockUser": "Engeli Kaldır", + "unblocked": "Engel kaldırıldı", + "blocked": "Engellendi", + "blockedSettingsTitle": "Engellenmiş kişiler", + "unbanUser": "Kullanıcı Engelini Kaldır", "unbanUserConfirm": "Are you sure you want to unban user?", - "userUnbanned": "User unbanned successfully", - "userUnbanFailed": "Unban failed!", + "userUnbanned": "Kullanıcının engeli başarıyla kaldırıldı", + "userUnbanFailed": "Engelini kaldırma başarısız!", "banUser": "Ban User", "banUserConfirm": "Are you sure you want to ban user?", "banUserAndDeleteAll": "Ban and Delete All", @@ -268,29 +263,29 @@ "leaveGroupConfirmationAdmin": "As you are the admin of this group, if you leave it it will be removed for every current members. Are you sure you want to leave this group?", "cannotRemoveCreatorFromGroup": "Cannot remove this user", "cannotRemoveCreatorFromGroupDesc": "You cannot remove this user as they are the creator of the group.", - "noContactsForGroup": "You don't have any contacts yet", - "failedToAddAsModerator": "Failed to add user as moderator", + "noContactsForGroup": "Henüz herhangi bir kişi yok", + "failedToAddAsModerator": "Kullanıcı moderatör olarak eklenemedi", "failedToRemoveFromModerator": "Failed to remove user from the moderator list", "copyMessage": "Copy message text", "selectMessage": "Select message", - "editGroup": "Edit group", - "editGroupName": "Edit group name", - "updateGroupDialogTitle": "Updating $name$...", - "showRecoveryPhrase": "Recovery Phrase", - "yourSessionID": "Your Session ID", - "setAccountPasswordTitle": "Set Account Password", - "setAccountPasswordDescription": "Require password to unlock Session’s screen. You can still receive message notifications while Screen Lock is enabled. Session’s notification settings allow you to customize information that is displayed", - "changeAccountPasswordTitle": "Change Account Password", - "changeAccountPasswordDescription": "Change your password", - "removeAccountPasswordTitle": "Remove Account Password", + "editGroup": "Grubu düzenle", + "editGroupName": "Grup adını düzenle", + "updateGroupDialogTitle": "$name$ güncelleniyor...", + "showRecoveryPhrase": "Kurtarma Sözcük Grubu", + "yourSessionID": "Session Kimliğiniz", + "setAccountPasswordTitle": "Hesap Şifresini Belirleyin", + "setAccountPasswordDescription": "Session ekranının kilidini açmak için parola iste. Ekran Kilidi etkinken mesaj bildirimleri almaya devam edebilirsiniz. Session’ın bildirim ayarları, görüntülenen bilgileri özelleştirmenize izin verir", + "changeAccountPasswordTitle": "Hesap Şifresini Değiştir", + "changeAccountPasswordDescription": "Şifrenizi değiştirin", + "removeAccountPasswordTitle": "Hesap Şifresini Kaldır", "removeAccountPasswordDescription": "Remove the password associated with your account", "enterPassword": "Please enter your password", - "confirmPassword": "Confirm password", + "confirmPassword": "Şifreyi Doğrula", "pasteLongPasswordToastTitle": "The clipboard content exceeds the maximum password length of $max_pwd_len$ characters.", "showRecoveryPhrasePasswordRequest": "Please enter your password", "recoveryPhraseSavePromptMain": "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don't give it to anyone.", - "invalidOpenGroupUrl": "Invalid URL", - "copiedToClipboard": "Copied to clipboard", + "invalidOpenGroupUrl": "Geçersiz URL", + "copiedToClipboard": "Panoya kopyalandı", "passwordViewTitle": "Type In Your Password", "unlock": "Unlock", "password": "Password", @@ -314,7 +309,7 @@ "removePasswordToastDescription": "You have removed your password.", "publicChatExists": "You are already connected to this open group", "connectToServerFail": "Couldn't join group", - "connectingToServer": "Connecting...", + "connectingToServer": "Bağlanılıyor...", "connectToServerSuccess": "Successfully connected to open group", "setPasswordFail": "Failed to set password", "passwordLengthError": "Password must be between 6 and 64 characters long", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json index ea4b3acd0..eaeca5348 100644 --- a/_locales/uk/messages.json +++ b/_locales/uk/messages.json @@ -1,7 +1,7 @@ { - "privacyPolicy": "Terms & Privacy Policy", + "privacyPolicy": "Умови та Політика конфіденційності", "copyErrorAndQuit": "Скопіювати помилку та вийти", - "unknown": "Unknown", + "unknown": "Невідомо", "databaseError": "Помилка бази даних", "mainMenuFile": "&Файл", "mainMenuEdit": "&Редагування", @@ -17,52 +17,52 @@ "editMenuCut": "Вирізати", "editMenuCopy": "Копіювати", "editMenuPaste": "Вставити", - "editMenuPasteAndMatchStyle": "Paste and Match Style", + "editMenuPasteAndMatchStyle": "Вставити в поточному стилі", "editMenuDelete": "Видалити", "editMenuSelectAll": "Вибрати все", "windowMenuClose": "Закрити вікно", "windowMenuMinimize": "Згорнути в трей", "windowMenuZoom": "Збільшити", - "windowMenuBringAllToFront": "Bring All to Front", + "windowMenuBringAllToFront": "Вивести все на передній план", "viewMenuResetZoom": "Актуальний розмір", "viewMenuZoomIn": "Збільшити", "viewMenuZoomOut": "Зменшити", "viewMenuToggleFullScreen": "Відкрити на повний екран", "viewMenuToggleDevTools": "Відкрити засоби розробника", - "contextMenuNoSuggestions": "No Suggestions", - "openGroupInvitation": "Open group invitation", - "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", - "enterSessionIDOrONSName": "Enter Session ID or ONS name", + "contextMenuNoSuggestions": "Немає припущень", + "openGroupInvitation": "Відкрити запрошення до групи", + "joinOpenGroupAfterInvitationConfirmationTitle": "Приєднатись до $roomName$?", + "joinOpenGroupAfterInvitationConfirmationDesc": "Ви впевнені, що хочете приєднатись до відкритої групи $roomName$?", + "enterSessionIDOrONSName": "Введіть ID сесії або ім'я ONS", "loading": "Оновлення…", "optimizingApplication": "Оптимізація програми...", - "done": "Done", + "done": "Готово", "me": "Я", "view": "Детальніше", "youLeftTheGroup": "Ви покинули групу", - "youGotKickedFromGroup": "You were removed from the group.", - "unreadMessage": "Unread Message", - "unreadMessages": "Unread Messages", + "youGotKickedFromGroup": "Вас вилучили із групи.", + "unreadMessage": "Непрочитане повідомлення", + "unreadMessages": "Непрочитані повідомлення", "debugLogExplanation": "Цей журнал буде доступний для розробників. Ви можете перевірити його перед відправкою.", "debugLogError": "Something went wrong with the upload! Please consider manually adding your log to the bug you file.", "reportIssue": "Повідомити про ваду", "gotIt": "Зрозуміло!", "submit": "Надіслати", - "markAllAsRead": "Mark All as Read", - "incomingError": "Error handling incoming message", - "media": "Media", + "markAllAsRead": "Відмітити все як прочитане", + "incomingError": "Помилка обробки вхідного повідомлення", + "media": "Медіа", "mediaEmptyState": "You don’t have any media in this conversation", - "documents": "Documents", + "documents": "Документи", "documentsEmptyState": "You don’t have any documents in this conversation", - "today": "Today", + "today": "Сьогодні", "yesterday": "Учора", "thisWeek": "This Week", "thisMonth": "Цей місяць", "voiceMessage": "Голосове повідомлення", "dangerousFileType": "Attachment type not allowed for security reasons", - "stagedPreviewThumbnail": "Draft thumbnail link preview for $domain$", - "previewThumbnail": "Thumbnail link preview for $domain$", - "stagedImageAttachment": "Draft image attachment: $path$", + "stagedPreviewThumbnail": "Попередній перегляд ескізу посилання для $domain$", + "previewThumbnail": "Попередній перегляд посилання для $domain$", + "stagedImageAttachment": "Ескіз прикріпленого зображення: $path$", "oneNonImageAtATimeToast": "When including a non-image attachment, the limit is one attachment per message.", "cannotMixImageAndNonImageAttachments": "You cannot mix non-image and image attachments in one message.", "maximumAttachments": "You cannot add any more attachments to this message.", @@ -73,49 +73,51 @@ "attemptingReconnection": "Спроба перепідключення через $reconnect_duration_in_seconds$ секунд", "submitDebugLog": "Debug log", "debugLog": "Журнал відладки", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", - "goToSupportPage": "Go to Support Page", - "menuReportIssue": "Report an Issue", + "goToSupportPage": "Перейти на сторінку підтримки", + "menuReportIssue": "Повідомити про помилку", "about": "About", "speech": "Розмова", "show": "Показати", - "sessionMessenger": "Session", + "sessionMessenger": "Сесія", "search": "Пошук", "noSearchResults": "No results for \"$searchTerm$\"", "conversationsHeader": "Розмови", "contactsHeader": "Контакти", "messagesHeader": "Повідомлення", - "settingsHeader": "Settings", - "typingAlt": "Typing animation for this conversation", - "contactAvatarAlt": "Avatar for contact $name$", - "downloadAttachment": "Download Attachment", + "settingsHeader": "Налаштування", + "typingAlt": "Анімація набору тексту для цієї розмови", + "contactAvatarAlt": "Аватар для контакту $name$", + "downloadAttachment": "Завантажити прикріплення", "replyToMessage": "Reply to Message", - "replyingToMessage": "Replying to:", - "originalMessageNotFound": "Original message not found", - "originalMessageNotAvailable": "Original message no longer available", - "messageFoundButNotLoaded": "Original message found, but not loaded. Scroll up to load it.", - "recording": "Recording", - "you": "You", + "replyingToMessage": "Відповідь на:", + "originalMessageNotFound": "Оригінальне повідомлення не знайдено", + "originalMessageNotAvailable": "Оригінальне повідомлення більше не доступне", + "messageFoundButNotLoaded": "Оригінальне повідомлення знайдено, але не завантажено. Прокрутіть вгору, щоб завантажити його.", + "recording": "Запис", + "you": "Ви", "audioPermissionNeededTitle": "Microphone access required", "audioPermissionNeeded": "To send audio messages, allow Session Desktop to access your microphone.", "audio": "Аудіо", - "video": "Video", - "photo": "Photo", - "cannotUpdate": "Cannot Update", + "video": "Відео", + "photo": "Фото", + "cannotUpdate": "Неможливо оновити", "cannotUpdateDetail": "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", "ok": "Добре", "cancel": "Відмінити", - "close": "Close", - "continue": "Continue", + "close": "Закрити", + "continue": "Продовжити", "error": "Помилка", "delete": "Видалити", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", - "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Видалити це повідомлення", - "deleted": "Deleted", + "messageDeletionForbidden": "У вас немає дозволу на видалення повідомлень інших", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Видалити повідомлення", + "deleted": "Видалено", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Від", "to": "to", "sent": "Надіслано", @@ -123,18 +125,10 @@ "sendMessage": "Надіслати повідомлення", "groupMembers": "Члени групи", "moreInformation": "More information", - "resend": "Resend", - "deleteMessage": "Видалити повідомлення", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Видалити повідомлення", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", + "resend": "Надіслати повторно", "deleteConversationConfirmation": "Видалити цю розмову без можливості відновлення?", - "clearAllData": "Clear All Data", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "clearAllData": "Очистити всі дані", + "deleteAccountWarning": "Це назавжди видалить повідомлення та контакти.", "deleteContactConfirmation": "Are you sure you want to delete this conversation?", "quoteThumbnailAlt": "Thumbnail of image from quoted message", "imageAttachmentAlt": "Image attached to message", @@ -142,10 +136,11 @@ "lightboxImageAlt": "Image sent in conversation", "imageCaptionIconAlt": "Icon showing that this image has a caption", "addACaption": "Add a caption...", - "copy": "Copy", + "copy": "Копіювати", "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Зберегти", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "Загальні", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -194,18 +189,18 @@ "timerOption_10_seconds": "10 секунд", "timerOption_30_seconds": "30 секунд", "timerOption_1_minute": "1 хвилина", - "timerOption_5_minutes": "5 minutes", - "timerOption_30_minutes": "30 minutes", + "timerOption_5_minutes": "5 хвилин", + "timerOption_30_minutes": "30 хвилин", "timerOption_1_hour": "1 година", - "timerOption_6_hours": "6 hours", - "timerOption_12_hours": "12 hours", + "timerOption_6_hours": "6 годин", + "timerOption_12_hours": "12 годин", "timerOption_1_day": "1 день", "timerOption_1_week": "1 тиждень", "disappearingMessages": "Зникаючі повідомлення ", - "changeNickname": "Change Nickname", + "changeNickname": "Змінити нікнейм", "clearNickname": "Clear nickname", - "nicknamePlaceholder": "New Nickname", - "changeNicknameMessage": "Enter a nickname for this user", + "nicknamePlaceholder": "Новий нікнейм", + "changeNicknameMessage": "Введіть нікнейм для цього користувача", "timerOption_0_seconds_abbreviated": "вимкн", "timerOption_5_seconds_abbreviated": "5с", "timerOption_10_seconds_abbreviated": "10с", @@ -222,7 +217,7 @@ "disabledDisappearingMessages": "$name$ disabled disappearing messages", "youDisabledDisappearingMessages": "You disabled disappearing messages", "timerSetTo": "Таймер встановленно на $time$", - "noteToSelf": "Note to Self", + "noteToSelf": "Замітка для себе", "hideMenuBarTitle": "Hide Menu Bar", "hideMenuBarDescription": "Toggle system menu bar visibility", "startConversation": "Почати нову розмову…", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/vi/messages.json b/_locales/vi/messages.json index 11845a04a..14f42aa27 100644 --- a/_locales/vi/messages.json +++ b/_locales/vi/messages.json @@ -73,6 +73,7 @@ "attemptingReconnection": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", "submitDebugLog": "Debug log", "debugLog": "Debug Log", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "Go to Release Notes", "goToSupportPage": "Go to Support Page", "menuReportIssue": "Report an Issue", @@ -109,13 +110,14 @@ "continue": "Tiếp tục", "error": "Lỗi", "delete": "Xóa", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "Xóa tin nhắn", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "Xóa tin nhắn", "deleted": "Deleted", + "messageDeletedPlaceholder": "This message has been deleted", "from": "Từ:", "to": "Đến:", "sent": "Đã gửi", @@ -124,14 +126,6 @@ "groupMembers": "Thành viên trong nhóm", "moreInformation": "More information", "resend": "Gửi lại", - "deleteMessage": "Xóa tin nhắn", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "Xóa tin nhắn", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "Xóa cuộc trò chuyện này vĩnh viễn?", "clearAllData": "Xóa tất cả dữ liệu", "deleteAccountWarning": "This will permanently delete your messages, and contacts.", @@ -146,6 +140,7 @@ "copySessionID": "Copy Session ID", "copyOpenGroupURL": "Copy Group's URL", "save": "Lưu", + "saveLogToDesktop": "Save log to desktop", "saved": "Saved", "permissions": "Permissions", "general": "Tổng quát", @@ -154,8 +149,8 @@ "linkPreviewsTitle": "Send Link Previews", "linkPreviewDescription": "Previews are supported for most urls", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "Allow access to camera and microphone", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "Spell Check", "spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDirty": "You must restart Session to apply your new settings", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 98f1f8267..f0774d0da 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -42,7 +42,7 @@ "youLeftTheGroup": "你已经离开了群组", "youGotKickedFromGroup": "您已被移出群组。", "unreadMessage": "未读消息", - "unreadMessages": "Unread Messages", + "unreadMessages": "未读消息", "debugLogExplanation": "该日志将公开发布以供参考,您可以在提交之前检查和编辑。", "debugLogError": "上传出错!您可以手动将日志添加到要反馈的bug中。", "reportIssue": "反馈错误信息", @@ -73,6 +73,7 @@ "attemptingReconnection": "在 $reconnect_duration_in_seconds$ 秒后尝试重新连接", "submitDebugLog": "调试日志", "debugLog": "调试日志", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "前往发布说明", "goToSupportPage": "前往支持页面", "menuReportIssue": "反馈问题", @@ -109,13 +110,14 @@ "continue": "继续", "error": "错误", "delete": "删除", - "deletePublicWarning": "您确定吗,对此公开群组中的每个人永久性删除本消息?", - "deleteMultiplePublicWarning": "您确定吗,对此公开群组中的每个人永久性删除这些消息?", - "deleteWarning": "点击“删除”将从本设备彻底删除此信息,您确定要删除吗?", - "deleteMultipleWarning": "您确定吗?点击“删除”将仅从此设备永久删除这些消息。", "messageDeletionForbidden": "您无权删除他人的对话消息", - "deleteThisMessage": "删除这条消息。", + "deleteJustForMe": "仅在我的设备上删除", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "删除消息", "deleted": "已删除", + "messageDeletedPlaceholder": "该消息已被删除", "from": "来自", "to": "向", "sent": "已发送", @@ -124,17 +126,9 @@ "groupMembers": "群组成员", "moreInformation": "更多信息", "resend": "重发", - "deleteMessage": "删除消息", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "删除消息", - "deleteMessageForEveryone": "对所有人删除消息", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "对所有人删除消息", - "deleteForEveryone": "Delete For Everyone", "deleteConversationConfirmation": "永久删除此会话?", "clearAllData": "清除所有数据", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", + "deleteAccountWarning": "这将永久删除您的消息和联系人", "deleteContactConfirmation": "您确定要删除此对话吗?", "quoteThumbnailAlt": "引用消息图片的缩略图", "imageAttachmentAlt": "消息图片", @@ -146,6 +140,7 @@ "copySessionID": "复制 Session ID", "copyOpenGroupURL": "复制群组链接", "save": "保存", + "saveLogToDesktop": "Save log to desktop", "saved": "已保存", "permissions": "权限", "general": "通用", @@ -153,9 +148,9 @@ "savedTheFile": "$name$ 保存了媒体内容。", "linkPreviewsTitle": "发送链接预览", "linkPreviewDescription": "多数链接都支持预览", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "麦克风和摄像头", - "mediaPermissionsDescription": "允许访问摄像头和麦克风", + "linkPreviewsConfirmMessage": "您无法在绝对的数据安全保障前提下发送链接预览。", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", "spellCheckTitle": "拼写检查", "spellCheckDescription": "启用输入框拼写检查", "spellCheckDirty": "您必须重新启动Session才能应用您的新设置", @@ -258,8 +253,8 @@ "userUnbanFailed": "解封失败!", "banUser": "封禁用户", "banUserConfirm": "Are you sure you want to ban user?", - "banUserAndDeleteAll": "Ban and Delete All", - "banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?", + "banUserAndDeleteAll": "屏蔽并全部删除", + "banUserAndDeleteAllConfirm": "您确定要屏蔽联系人并删除他的所有消息吗?", "userBanned": "User banned successfully", "userBanFailed": "封禁失败!", "leaveGroup": "离开群聊", @@ -340,8 +335,8 @@ "onlyAdminCanRemoveMembersDesc": "只有群组的创建者可以移除用户", "createAccount": "Create Account", "signIn": "登录", - "startInTrayTitle": "Start in Tray", - "startInTrayDescription": "Start Session as a minified app ", + "startInTrayTitle": "从托盘中启动", + "startInTrayDescription": "将 Session 启动为最小化应用程序 ", "yourUniqueSessionID": "向您的Session ID打个招呼吧", "allUsersAreRandomly...": "您的Session ID是其他用户在与您聊天时使用的独一无二的地址。Session ID与您的真实身份无关,它在设计上完全是匿名且私密的。", "getStarted": "开始使用", @@ -361,7 +356,7 @@ "beginYourSession": "开始<您的会话。", "welcomeToYourSession": "欢迎来到 Session 。", "newSession": "新建私人聊天", - "searchFor...": "Search for conversations or contacts", + "searchFor...": "搜索对话内容或联系人", "enterSessionID": "输入Session ID", "enterSessionIDOfRecipient": "输入对方的Session ID 或者 ONS 名称", "usersCanShareTheir...": "用户可以通过进入帐号设置并点击\"共享Session ID\"来分享自己的Session ID,或通过共享其二维码来分享其Session ID。", @@ -417,44 +412,55 @@ "clickToTrustContact": "点击下载文件", "trustThisContactDialogTitle": "是否信任 $name$?", "trustThisContactDialogDescription": "您确定要下载$name$发送的媒体消息吗?", - "pinConversation": "Pin Conversation", - "unpinConversation": "Unpin Conversation", - "pinConversationLimitTitle": "Pinned conversations limit", - "pinConversationLimitToastDescription": "You can only pin $number$ conversations", - "latestUnreadIsAbove": "First unread message is above", - "sendRecoveryPhraseTitle": "Sending Recovery Phrase", - "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", - "dialogClearAllDataDeletionFailedTitle": "Data not deleted", - "dialogClearAllDataDeletionFailedDesc": "Data not deleted with an unknown error. Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedTitleQuestion": "Do you want to delete data from just this device?", - "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", - "dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?", + "pinConversation": "置顶对话", + "unpinConversation": "取消置顶对话", + "pinConversationLimitTitle": "置顶对话上限", + "pinConversationLimitToastDescription": "你只能置顶 $number$ 个聊天", + "showUserDetails": "Show User Details", + "latestUnreadIsAbove": "第一条未读消息在上方", + "sendRecoveryPhraseTitle": "正在发送恢复口令", + "sendRecoveryPhraseMessage": "您正在尝试发送恢复口令,它能用来登录和访问您的账号。\n您确定要发送该消息吗?", + "dialogClearAllDataDeletionFailedTitle": "未删除数据", + "dialogClearAllDataDeletionFailedDesc": "出现未知错误,数据删除失败。您想要仅在此设备中删除数据吗?", + "dialogClearAllDataDeletionFailedTitleQuestion": "您想要仅在此设备中删除数据吗?", + "dialogClearAllDataDeletionFailedMultiple": "数据删除失败。节点错误: $snodes$", + "dialogClearAllDataDeletionQuestion": "您想仅清空此设备,还是删除整个账号?", "deviceOnly": "仅设备", - "entireAccount": "Entire Account", - "areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?", - "areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?", - "iAmSure": "I am sure", - "recoveryPhraseSecureTitle": "You're almost finished!", - "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", - "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", - "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", - "surveyTitle": "Take our Session Survey", - "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", - "accept": "Accept", - "decline": "Decline", - "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", - "unableToCallTitle": "Cannot start new call", - "callMissed": "Missed call from $name$", - "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", - "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "entireAccount": "整个账号", + "areYouSureDeleteDeviceOnly": "是否仅删除这台设备的数据?", + "areYouSureDeleteEntireAccount": "是否仅删除您的整个账号,包括网络数据?", + "iAmSure": "确认", + "recoveryPhraseSecureTitle": "就快要完成了!", + "recoveryPhraseRevealMessage": "通过发送恢复口令,保障您账号安全。展示恢复口令,并将其储存到安全的地方。", + "recoveryPhraseRevealButtonText": "展示恢复口令", + "notificationSubtitle": "通知设置 - $setting$", + "surveyTitle": "参加我们的产品问卷调研", + "goToOurSurvey": "跳转至问卷", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", + "accept": "接受", + "decline": "拒绝", + "endCall": "结束呼叫", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", + "unableToCallTitle": "无法开始新通话", + "callMissed": "来自 $name$ 的未接来电", + "callMissedTitle": "未接来电", + "noCameraFound": "找不到摄像头", + "noAudioInputFound": "找不到音频输入", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index ca28a2971..8779e4a7f 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -30,25 +30,25 @@ "viewMenuToggleFullScreen": "切換全螢幕", "viewMenuToggleDevTools": "切換開發者工具", "contextMenuNoSuggestions": "沒有建議", - "openGroupInvitation": "Open group invitation", - "joinOpenGroupAfterInvitationConfirmationTitle": "Join $roomName$?", - "joinOpenGroupAfterInvitationConfirmationDesc": "Are you sure you want to join the $roomName$ open group?", - "enterSessionIDOrONSName": "Enter Session ID or ONS name", + "openGroupInvitation": "打開群組邀請", + "joinOpenGroupAfterInvitationConfirmationTitle": "加入 $roomName$?", + "joinOpenGroupAfterInvitationConfirmationDesc": "您確定要加入 $roomName$ 公開群組嗎?", + "enterSessionIDOrONSName": "請輸入您的 ID 或 ONS 的名稱", "loading": "載入中...", "optimizingApplication": "正在最佳化應用程式...", - "done": "Done", + "done": "完成", "me": "我", "view": "檢視", "youLeftTheGroup": "你已離開此群組", - "youGotKickedFromGroup": "You were removed from the group.", - "unreadMessage": "Unread Message", - "unreadMessages": "Unread Messages", + "youGotKickedFromGroup": "您已從群組中移除", + "unreadMessage": "未讀訊息", + "unreadMessages": "未讀訊息", "debugLogExplanation": "這個紀錄將被公開張貼在線上給貢獻者瀏覽。在送出前,你可以檢視及修改。", "debugLogError": "上傳出問題目! 請考慮手動加入活動記錄檔來查錯", "reportIssue": "回報問題", "gotIt": "了解!", "submit": "送出", - "markAllAsRead": "Mark All as Read", + "markAllAsRead": "全部標記為已讀", "incomingError": "在處理來訊時出現錯誤", "media": "媒體", "mediaEmptyState": "您在本次對話中無任何媒體檔案", @@ -73,10 +73,11 @@ "attemptingReconnection": "嘗試在 $reconnect_duration_in_seconds$ 秒內重新連線", "submitDebugLog": "除錯紀錄", "debugLog": "除錯日誌", + "showDebugLog": "Show Debug Log", "goToReleaseNotes": "前往發行紀錄", "goToSupportPage": "前往支援頁面", "menuReportIssue": "回報問題", - "about": "About", + "about": "關於", "speech": "語音", "show": "顯示", "sessionMessenger": "Session", @@ -85,86 +86,80 @@ "conversationsHeader": "對話", "contactsHeader": "聯絡人", "messagesHeader": "訊息", - "settingsHeader": "Settings", + "settingsHeader": "設定", "typingAlt": "本次對話中輸入動畫", "contactAvatarAlt": "聯絡人 $name$ 頭像圖示", "downloadAttachment": "下載附件", "replyToMessage": "回覆訊息", - "replyingToMessage": "Replying to:", + "replyingToMessage": "回復:", "originalMessageNotFound": "找不到原始訊息", "originalMessageNotAvailable": "原始訊息無法再取得", "messageFoundButNotLoaded": "找到了原始訊息,但未載入,向上滑動以進行載入。", - "recording": "Recording", + "recording": "錄製中", "you": "你", "audioPermissionNeededTitle": "Microphone access required", "audioPermissionNeeded": "要傳送語音訊息,須授權 Siganl 桌面版可以使用您設備的麥克風。 ", "audio": "聲音", "video": "影片", "photo": "照片", - "cannotUpdate": "Cannot Update", - "cannotUpdateDetail": "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", + "cannotUpdate": "無法更新", + "cannotUpdateDetail": "Session更新失敗,但仍有其他適用的新版本。請到https://getsession.org/上手動下載,然後聯繫客服支持或提交出錯版本歸檔。", "ok": "好", "cancel": "取消", - "close": "Close", - "continue": "Continue", + "close": "關閉", + "continue": "繼續", "error": "錯誤", "delete": "刪除", - "deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.", - "deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.", - "deleteWarning": "您確定嗎?點擊「刪除」只會自本台設備上永久地移除此訊息。", - "deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.", - "messageDeletionForbidden": "You don’t have permission to delete others’ messages", - "deleteThisMessage": "刪除這則訊息", - "deleted": "Deleted", + "messageDeletionForbidden": "您無權刪除他人訊息", + "deleteJustForMe": "Delete just for me", + "deleteForEveryone": "Delete for everyone", + "deleteMessagesQuestion": "Delete those messages?", + "deleteMessageQuestion": "Delete this message?", + "deleteMessages": "刪除訊息", + "deleted": "已刪除", + "messageDeletedPlaceholder": "This message has been deleted", "from": "來自", "to": "至", "sent": "已送出", "received": "已接收", "sendMessage": "送出一則訊息", "groupMembers": "群組成員", - "moreInformation": "More information", - "resend": "Resend", - "deleteMessage": "刪除訊息", - "deleteMessageQuestion": "Delete message?", - "deleteMessagesQuestion": "Delete messages?", - "deleteMessages": "刪除訊息", - "deleteMessageForEveryone": "Delete Message For Everyone", - "deleteMessageForEveryoneLowercase": "Delete Message For Everyone", - "deleteMessagesForEveryone": "Delete Messages For Everyone", - "deleteForEveryone": "Delete For Everyone", + "moreInformation": "更多信息", + "resend": "重傳", "deleteConversationConfirmation": "永久刪除對話?", - "clearAllData": "Clear All Data", - "deleteAccountWarning": "This will permanently delete your messages, and contacts.", - "deleteContactConfirmation": "Are you sure you want to delete this conversation?", + "clearAllData": "清除所有資料", + "deleteAccountWarning": "這樣做將永久清除您的訊息與聯絡人。", + "deleteContactConfirmation": "確定刪除此會話?", "quoteThumbnailAlt": "引用訊息的縮圖", "imageAttachmentAlt": "訊息裏插入的圖片", "videoAttachmentAlt": "伴隨訊息的影片截圖", "lightboxImageAlt": "會話中送出的圖片", "imageCaptionIconAlt": "圖標顯示此圖像有說明文字", "addACaption": "加入一個標題...", - "copy": "Copy", - "copySessionID": "Copy Session ID", - "copyOpenGroupURL": "Copy Group's URL", + "copy": "複製", + "copySessionID": "複製Session ID", + "copyOpenGroupURL": "複製群組 URL", "save": "儲存", - "saved": "Saved", + "saveLogToDesktop": "Save log to desktop", + "saved": "已儲存", "permissions": "許可", "general": "一般", - "tookAScreenshot": "$name$ took a screenshot", - "savedTheFile": "Media saved by $name$", - "linkPreviewsTitle": "Send Link Previews", - "linkPreviewDescription": "Previews are supported for most urls", - "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", - "mediaPermissionsTitle": "Microphone and Camera", - "mediaPermissionsDescription": "授權軟體可以使用相機與麥克風功能", - "spellCheckTitle": "Spell Check", + "tookAScreenshot": "$name$ 擷取了螢幕畫面", + "savedTheFile": "$name$ 儲存了媒體", + "linkPreviewsTitle": "傳送連結預覽", + "linkPreviewDescription": "大部分網址都支援預覽功能", + "linkPreviewsConfirmMessage": "您在傳送連結預覽時無法得到完整的元數據保護。", + "mediaPermissionsTitle": "Microphone", + "mediaPermissionsDescription": "Allow access to microphone", + "spellCheckTitle": "拼寫檢查", "spellCheckDescription": "在訊息撰寫時啟用文字輸入的拼字檢查", - "spellCheckDirty": "You must restart Session to apply your new settings", + "spellCheckDirty": "您必須重新啓動 Session 以應用您的新設定", "notifications": "通知", "readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).", - "readReceiptSettingTitle": "Read Receipts", + "readReceiptSettingTitle": "已讀回條", "typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).", - "typingIndicatorsSettingTitle": "Typing Indicators", - "zoomFactorSettingTitle": "Zoom Factor", + "typingIndicatorsSettingTitle": "輸入狀態", + "zoomFactorSettingTitle": "縮放係數", "notificationSettingsDialog": "當訊息抵達時,通知會顯示:", "disableNotifications": "關閉通知", "nameAndMessage": "傳送者與訊息", @@ -183,7 +178,7 @@ "timestamp_m": "1 分鐘", "timestamp_h": "1 小時", "timestampFormat_M": "MMM D", - "messageBodyMissing": "Please enter a message body.", + "messageBodyMissing": "請輸入訊息內容。", "unblockToSend": "解鎖聯絡人來傳送訊息。", "unblockGroupToSend": "解除此群組封鎖以傳送訊息。", "youChangedTheTimer": "您設定訊息讀後焚毀的時間為 $time$", @@ -202,10 +197,10 @@ "timerOption_1_day": "1 天", "timerOption_1_week": "1 週", "disappearingMessages": "自動銷毀訊息", - "changeNickname": "Change Nickname", + "changeNickname": "變更暱稱", "clearNickname": "Clear nickname", - "nicknamePlaceholder": "New Nickname", - "changeNicknameMessage": "Enter a nickname for this user", + "nicknamePlaceholder": "新暱稱", + "changeNicknameMessage": "爲該用戶鍵入昵稱", "timerOption_0_seconds_abbreviated": "關閉", "timerOption_5_seconds_abbreviated": "5 秒", "timerOption_10_seconds_abbreviated": "10 秒", @@ -223,77 +218,77 @@ "youDisabledDisappearingMessages": "您關閉了訊息讀後焚毀功能", "timerSetTo": "計時器設定為 $time$", "noteToSelf": "給自己的筆記", - "hideMenuBarTitle": "Hide Menu Bar", - "hideMenuBarDescription": "Toggle system menu bar visibility", + "hideMenuBarTitle": "隱藏選單列", + "hideMenuBarDescription": "切換系統選單列可見性", "startConversation": "開始新的對話...", "invalidNumberError": "無效號碼", - "failedResolveOns": "Failed to resolve ONS name", - "successUnlinked": "Your device was unlinked successfully", - "autoUpdateSettingTitle": "Auto Update", + "failedResolveOns": "無法解析 ONS 名稱", + "successUnlinked": "成功解除連結您的裝置", + "autoUpdateSettingTitle": "自動更新", "autoUpdateSettingDescription": "Automatically check for updates on launch", "autoUpdateNewVersionTitle": "Session 可用的更新", "autoUpdateNewVersionMessage": "這是新版本的 Session", "autoUpdateNewVersionInstructions": "點選重啟 Session 來套用更新。", "autoUpdateRestartButtonLabel": "重啟 Session", "autoUpdateLaterButtonLabel": "稍後", - "autoUpdateDownloadButtonLabel": "Download", + "autoUpdateDownloadButtonLabel": "下載", "autoUpdateDownloadedMessage": "The new update has been downloaded.", - "autoUpdateDownloadInstructions": "Would you like to download the update?", + "autoUpdateDownloadInstructions": "您要下載更新嗎?", "leftTheGroup": "$name$ 離開此群組", "multipleLeftTheGroup": "$name$ 離開此群組", "updatedTheGroup": "群組昇級", "titleIsNow": "標題現為 '$name$'", "joinedTheGroup": "$name$ 已加入小組", "multipleJoinedTheGroup": "$names$ 已加入群組。", - "kickedFromTheGroup": "$name$ was removed from the group.", - "multipleKickedFromTheGroup": "$name$ were removed from the group.", - "blockUser": "Block", - "unblockUser": "Unblock", - "unblocked": "Unblocked", - "blocked": "Blocked", - "blockedSettingsTitle": "Blocked contacts", - "unbanUser": "Unban User", + "kickedFromTheGroup": "$name$ 已被移出群組。", + "multipleKickedFromTheGroup": "$name$ 已被移出群組。", + "blockUser": "封鎖", + "unblockUser": "解除封鎖", + "unblocked": "解除封鎖", + "blocked": "已封鎖", + "blockedSettingsTitle": "已封鎖的聯絡人", + "unbanUser": "解除封鎖用戶", "unbanUserConfirm": "Are you sure you want to unban user?", - "userUnbanned": "User unbanned successfully", - "userUnbanFailed": "Unban failed!", - "banUser": "Ban User", + "userUnbanned": "已解除封鎖用戶", + "userUnbanFailed": "解除封鎖失敗!", + "banUser": "封鎖用戶", "banUserConfirm": "Are you sure you want to ban user?", - "banUserAndDeleteAll": "Ban and Delete All", - "banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?", + "banUserAndDeleteAll": "封鎖並刪除所有", + "banUserAndDeleteAllConfirm": "您要封鎖該用戶並刪除其所有訊息嗎?", "userBanned": "User banned successfully", - "userBanFailed": "Ban failed!", - "leaveGroup": "Leave Group", - "leaveAndRemoveForEveryone": "Leave Group and remove for everyone", - "leaveGroupConfirmation": "Are you sure you want to leave this group?", + "userBanFailed": "封鎖失敗!", + "leaveGroup": "離開群組", + "leaveAndRemoveForEveryone": "離開群組並爲所有人移除", + "leaveGroupConfirmation": "您確定要離開此群組?", "leaveGroupConfirmationAdmin": "As you are the admin of this group, if you leave it it will be removed for every current members. Are you sure you want to leave this group?", - "cannotRemoveCreatorFromGroup": "Cannot remove this user", - "cannotRemoveCreatorFromGroupDesc": "You cannot remove this user as they are the creator of the group.", - "noContactsForGroup": "You don't have any contacts yet", - "failedToAddAsModerator": "Failed to add user as moderator", + "cannotRemoveCreatorFromGroup": "不能移除該用戶", + "cannotRemoveCreatorFromGroupDesc": "您不能從群組中移除群組創建者。", + "noContactsForGroup": "您尚未添加聯絡人", + "failedToAddAsModerator": "新增用戶爲版主失敗", "failedToRemoveFromModerator": "Failed to remove user from the moderator list", - "copyMessage": "Copy message text", - "selectMessage": "Select message", - "editGroup": "Edit group", - "editGroupName": "Edit group name", - "updateGroupDialogTitle": "Updating $name$...", - "showRecoveryPhrase": "Recovery Phrase", - "yourSessionID": "Your Session ID", - "setAccountPasswordTitle": "Set Account Password", + "copyMessage": "複製訊息文字", + "selectMessage": "選取訊息", + "editGroup": "編輯群組", + "editGroupName": "編輯群組名稱", + "updateGroupDialogTitle": "更新 $name$...", + "showRecoveryPhrase": "回復用字句", + "yourSessionID": "您的 Session ID", + "setAccountPasswordTitle": "設置賬戶密碼", "setAccountPasswordDescription": "Require password to unlock Session’s screen. You can still receive message notifications while Screen Lock is enabled. Session’s notification settings allow you to customize information that is displayed", - "changeAccountPasswordTitle": "Change Account Password", - "changeAccountPasswordDescription": "Change your password", - "removeAccountPasswordTitle": "Remove Account Password", + "changeAccountPasswordTitle": "更改帳戶密碼", + "changeAccountPasswordDescription": "變更您的密碼", + "removeAccountPasswordTitle": "移除帳戶密碼", "removeAccountPasswordDescription": "Remove the password associated with your account", - "enterPassword": "Please enter your password", - "confirmPassword": "Confirm password", + "enterPassword": "請輸入您的密碼", + "confirmPassword": "確認密碼", "pasteLongPasswordToastTitle": "The clipboard content exceeds the maximum password length of $max_pwd_len$ characters.", - "showRecoveryPhrasePasswordRequest": "Please enter your password", + "showRecoveryPhrasePasswordRequest": "請輸入您的密碼", "recoveryPhraseSavePromptMain": "Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don't give it to anyone.", - "invalidOpenGroupUrl": "Invalid URL", - "copiedToClipboard": "Copied to clipboard", - "passwordViewTitle": "Type In Your Password", + "invalidOpenGroupUrl": "無效 URL", + "copiedToClipboard": "已複製到剪貼簿", + "passwordViewTitle": "輸入你的密碼", "unlock": "Unlock", - "password": "Password", + "password": "密碼", "setPassword": "Set Password", "changePassword": "Change Password", "removePassword": "Remove Password", @@ -302,25 +297,25 @@ "invalidOldPassword": "Old password is invalid", "invalidPassword": "Invalid password", "noGivenPassword": "Please enter your password", - "passwordsDoNotMatch": "Passwords do not match", - "setPasswordInvalid": "Passwords do not match", + "passwordsDoNotMatch": "密碼不一致", + "setPasswordInvalid": "密碼不一致", "changePasswordInvalid": "The old password you entered is incorrect", "removePasswordInvalid": "Incorrect password", - "setPasswordTitle": "Set Password", - "changePasswordTitle": "Changed Password", - "removePasswordTitle": "Removed Password", + "setPasswordTitle": "設定密碼", + "changePasswordTitle": "變更密碼", + "removePasswordTitle": "移除密碼", "setPasswordToastDescription": "Your password has been set. Please keep it safe.", "changePasswordToastDescription": "Your password has been changed. Please keep it safe.", "removePasswordToastDescription": "You have removed your password.", "publicChatExists": "You are already connected to this open group", - "connectToServerFail": "Couldn't join group", - "connectingToServer": "Connecting...", - "connectToServerSuccess": "Successfully connected to open group", + "connectToServerFail": "無法加入群組", + "connectingToServer": "連線中", + "connectToServerSuccess": "成功連線至開放群組。", "setPasswordFail": "Failed to set password", "passwordLengthError": "Password must be between 6 and 64 characters long", "passwordTypeError": "Password must be a string", "passwordCharacterError": "Password must only contain letters, numbers and symbols", - "remove": "Remove", + "remove": "移除", "invalidSessionId": "Invalid Session ID", "invalidPubkeyFormat": "Invalid Pubkey Format", "emptyGroupNameError": "Please enter a group name", @@ -421,6 +416,7 @@ "unpinConversation": "Unpin Conversation", "pinConversationLimitTitle": "Pinned conversations limit", "pinConversationLimitToastDescription": "You can only pin $number$ conversations", + "showUserDetails": "Show User Details", "latestUnreadIsAbove": "First unread message is above", "sendRecoveryPhraseTitle": "Sending Recovery Phrase", "sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?", @@ -438,23 +434,33 @@ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "notificationSubtitle": "Notifications - $setting$", - "deletionTypeTitle": "Deletion Type", - "deleteJustForMe": "Delete just for me", - "messageDeletedPlaceholder": "This message has been deleted", - "messageDeleted": "Message deleted", "surveyTitle": "Take our Session Survey", "goToOurSurvey": "Go to our survey", - "incomingCall": "Incoming call", + "blockAll": "Block All", + "messageRequests": "Message Requests", + "requestsSubtitle": "Pending Requests", + "requestsPlaceholder": "No requests", + "messageRequestsDescription": "Enable Message Request Inbox", + "incomingCallFrom": "Incoming call from '$name$'", + "ringing": "Ringing...", + "establishingConnection": "Establishing connection...", "accept": "Accept", "decline": "Decline", "endCall": "End call", - "micAndCameraPermissionNeededTitle": "Camera and Microphone access required", - "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", - "unableToCall": "cancel your ongoing call first", + "cameraPermissionNeededTitle": "Voice/Video Call permissions required", + "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.", + "unableToCall": "Cancel your ongoing call first", "unableToCallTitle": "Cannot start new call", "callMissed": "Missed call from $name$", "callMissedTitle": "Call missed", - "startVideoCall": "Start Video Call", "noCameraFound": "No camera found", - "noAudioInputFound": "No audio input found" + "noAudioInputFound": "No audio input found", + "noAudioOutputFound": "No audio output found", + "callMediaPermissionsTitle": "Voice and video calls", + "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", + "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", + "menuCall": "Call", + "startedACall": "You called $name$", + "answeredACall": "Call with $name$" } diff --git a/tools/updateI18nKeysType.py b/tools/updateI18nKeysType.py new file mode 100755 index 000000000..f425ba828 --- /dev/null +++ b/tools/updateI18nKeysType.py @@ -0,0 +1,28 @@ +#!/bin/python3 + + +# usage : ./tools/compareLocalizedStrings.py en de + +import re +from os import path, listdir +from glob import glob +import json +import sys + +LOCALES_FOLDER = './_locales' + +EN_FILE = LOCALES_FOLDER + '/en/messages.json' + +LOCALIZED_KEYS_FILE = './ts/types/LocalizerKeys.ts' + +stringToWrite = "export type LocalizerKeys =\n | " + +with open(EN_FILE,'r') as jsonFile: + data = json.load(jsonFile) + keys = data.keys() + + stringToWrite += json.dumps(keys, sort_keys=True).replace(',', '\n |').replace('"', '\'')[1:-1] + print(stringToWrite) + with open(LOCALIZED_KEYS_FILE, "w") as typeFile: + typeFile.write(stringToWrite) + diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 3a4bfc1ca..dc6d191df 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -74,7 +74,7 @@ const AvatarImage = (props: { {window.i18n('contactAvatarAlt', ); diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 6e2622933..43b3270fe 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -281,7 +281,7 @@ const ConversationHeaderTitle = () => { } const notificationSubtitle = notificationSetting - ? window.i18n('notificationSubtitle', notificationSetting) + ? window.i18n('notificationSubtitle', [notificationSetting]) : null; const fullTextSubtitle = memberCountText ? `${memberCountText} ● ${notificationSubtitle}` diff --git a/ts/components/conversation/DataExtractionNotification.tsx b/ts/components/conversation/DataExtractionNotification.tsx index c19ddba9e..1b6d1928e 100644 --- a/ts/components/conversation/DataExtractionNotification.tsx +++ b/ts/components/conversation/DataExtractionNotification.tsx @@ -11,9 +11,9 @@ export const DataExtractionNotification = (props: PropsForDataExtractionNotifica let contentText: string; if (type === SignalService.DataExtractionNotification.Type.MEDIA_SAVED) { - contentText = window.i18n('savedTheFile', name || source); + contentText = window.i18n('savedTheFile', [name || source]); } else { - contentText = window.i18n('tookAScreenshot', name || source); + contentText = window.i18n('tookAScreenshot', [name || source]); } return ( diff --git a/ts/components/conversation/GroupNotification.tsx b/ts/components/conversation/GroupNotification.tsx index d82e37f3a..bb9688b8b 100644 --- a/ts/components/conversation/GroupNotification.tsx +++ b/ts/components/conversation/GroupNotification.tsx @@ -27,43 +27,44 @@ function getPeople(change: TypeWithContacts) { return change.contacts?.map(c => c.profileName || c.pubkey).join(', '); } +// tslint:disable-next-line: cyclomatic-complexity const ChangeItem = (change: PropsForGroupUpdateType): string => { - const people = isTypeWithContact(change) ? getPeople(change) : []; + const people = isTypeWithContact(change) ? getPeople(change) : undefined; switch (change.type) { case 'name': - return window.i18n('titleIsNow', change.newName || ''); + return window.i18n('titleIsNow', [change.newName || '']); case 'add': - if (!change.contacts || !change.contacts.length) { + if (!change.contacts || !change.contacts.length || !people) { throw new Error('Group update add is missing contacts'); } const joinKey = change.contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup'; - return window.i18n(joinKey, people); + return window.i18n(joinKey, [people]); case 'remove': if (change.isMe) { return window.i18n('youLeftTheGroup'); } - if (!change.contacts || !change.contacts.length) { + if (!change.contacts || !change.contacts.length || !people) { throw new Error('Group update remove is missing contacts'); } const leftKey = change.contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup'; - return window.i18n(leftKey, people); + return window.i18n(leftKey, [people]); case 'kicked': if (change.isMe) { return window.i18n('youGotKickedFromGroup'); } - if (!change.contacts || !change.contacts.length) { + if (!change.contacts || !change.contacts.length || !people) { throw new Error('Group update kicked is missing contacts'); } const kickedKey = change.contacts.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup'; - return window.i18n(kickedKey, people); + return window.i18n(kickedKey, [people]); case 'general': return window.i18n('updatedTheGroup'); diff --git a/ts/components/conversation/StagedLinkPreview.tsx b/ts/components/conversation/StagedLinkPreview.tsx index 57c4803a0..36f9d7a1a 100644 --- a/ts/components/conversation/StagedLinkPreview.tsx +++ b/ts/components/conversation/StagedLinkPreview.tsx @@ -38,7 +38,7 @@ export const StagedLinkPreview = (props: Props) => { {isLoaded && image && isImage ? (
{window.i18n('stagedPreviewThumbnail', { const convo = getConversationController().get(sender); window.inboxStore?.dispatch( updateConfirmModal({ - title: window.i18n( - 'trustThisContactDialogTitle', - convo.getContactProfileNameOrShortenedPubKey() - ), - message: window.i18n( - 'trustThisContactDialogDescription', - convo.getContactProfileNameOrShortenedPubKey() - ), + title: window.i18n('trustThisContactDialogTitle', [ + convo.getContactProfileNameOrShortenedPubKey(), + ]), + message: window.i18n('trustThisContactDialogDescription', [ + convo.getContactProfileNameOrShortenedPubKey(), + ]), okTheme: SessionButtonColor.Green, onClickOk: async () => { convo.set({ isTrustedForAttachmentDownload: true }); diff --git a/ts/components/conversation/notification-bubble/CallNotification.tsx b/ts/components/conversation/notification-bubble/CallNotification.tsx index 9302a157a..0015e005f 100644 --- a/ts/components/conversation/notification-bubble/CallNotification.tsx +++ b/ts/components/conversation/notification-bubble/CallNotification.tsx @@ -4,13 +4,14 @@ import { PubKey } from '../../../session/types'; import { CallNotificationType, PropsForCallNotification } from '../../../state/ducks/conversations'; import { getSelectedConversation } from '../../../state/selectors/conversations'; +import { LocalizerKeys } from '../../../types/LocalizerKeys'; import { SessionIconType } from '../../session/icon'; import { ReadableMessage } from '../ReadableMessage'; import { NotificationBubble } from './NotificationBubble'; type StyleType = Record< CallNotificationType, - { notificationTextKey: string; iconType: SessionIconType; iconColor: string } + { notificationTextKey: LocalizerKeys; iconType: SessionIconType; iconColor: string } >; const style: StyleType = { @@ -42,7 +43,7 @@ export const CallNotification = (props: PropsForCallNotification) => { (selectedConvoProps?.id && PubKey.shorten(selectedConvoProps?.id)); const styleItem = style[notificationType]; - const notificationText = window.i18n(styleItem.notificationTextKey, displayName); + const notificationText = window.i18n(styleItem.notificationTextKey, [displayName || 'Unknown']); if (!window.i18n(styleItem.notificationTextKey)) { throw new Error(`invalid i18n key ${styleItem.notificationTextKey}`); } diff --git a/ts/components/dialog/DeleteAccountModal.tsx b/ts/components/dialog/DeleteAccountModal.tsx index 90bf1c12b..ec10dcae5 100644 --- a/ts/components/dialog/DeleteAccountModal.tsx +++ b/ts/components/dialog/DeleteAccountModal.tsx @@ -89,10 +89,9 @@ async function deleteEverythingAndNetworkData() { window.inboxStore?.dispatch( updateConfirmModal({ title: window.i18n('dialogClearAllDataDeletionFailedTitle'), - message: window.i18n( - 'dialogClearAllDataDeletionFailedMultiple', - potentiallyMaliciousSnodes.join(', ') - ), + message: window.i18n('dialogClearAllDataDeletionFailedMultiple', [ + potentiallyMaliciousSnodes.join(', '), + ]), messageSub: window.i18n('dialogClearAllDataDeletionFailedTitleQuestion'), okTheme: SessionButtonColor.Danger, okText: window.i18n('deviceOnly'), diff --git a/ts/components/dialog/InviteContactsDialog.tsx b/ts/components/dialog/InviteContactsDialog.tsx index 8307fc562..a21ab503b 100644 --- a/ts/components/dialog/InviteContactsDialog.tsx +++ b/ts/components/dialog/InviteContactsDialog.tsx @@ -107,7 +107,7 @@ const InviteContactsDialogInner = (props: Props) => { const completeUrl = await getCompleteUrlForV2ConvoId(convo.id); const groupInvitation = { url: completeUrl, - name: convo.getName(), + name: convo.getName() || 'Unknown', }; pubkeys.forEach(async pubkeyStr => { const privateConvo = await getConversationController().getOrCreateAndWait( diff --git a/ts/components/dialog/SessionPasswordDialog.tsx b/ts/components/dialog/SessionPasswordDialog.tsx index a9d4edc1c..2cdd3506e 100644 --- a/ts/components/dialog/SessionPasswordDialog.tsx +++ b/ts/components/dialog/SessionPasswordDialog.tsx @@ -8,6 +8,7 @@ import { SessionWrapperModal } from '../session/SessionWrapperModal'; import { SpacerLG, SpacerSM } from '../basic/Text'; import autoBind from 'auto-bind'; import { sessionPassword } from '../../state/ducks/modalDialog'; +import { LocalizerKeys } from '../../types/LocalizerKeys'; export type PasswordAction = 'set' | 'change' | 'remove'; interface Props { @@ -58,12 +59,16 @@ export class SessionPasswordDialog extends React.Component { const confirmButtonColor = passwordAction === 'remove' ? SessionButtonColor.Danger : SessionButtonColor.Green; + // do this separately so typescript's compiler likes it + const localizedKeyAction: LocalizerKeys = + passwordAction === 'change' + ? 'changePassword' + : passwordAction === 'remove' + ? 'removePassword' + : 'setPassword'; return ( - +
diff --git a/ts/components/dialog/UpdateGroupMembersDialog.tsx b/ts/components/dialog/UpdateGroupMembersDialog.tsx index da94fc094..74dc98e98 100644 --- a/ts/components/dialog/UpdateGroupMembersDialog.tsx +++ b/ts/components/dialog/UpdateGroupMembersDialog.tsx @@ -139,7 +139,7 @@ export class UpdateGroupMembersDialog extends React.Component { const okText = window.i18n('ok'); const cancelText = window.i18n('cancel'); - const titleText = window.i18n('updateGroupDialogTitle', this.convo.getName()); + const titleText = window.i18n('updateGroupDialogTitle', [this.convo.getName() || '']); return ( { const groupId = this.convo.id; const groupName = this.convo.getName(); - void ClosedGroup.initiateGroupUpdate(groupId, groupName, filteredMembers, avatarPath); + void ClosedGroup.initiateGroupUpdate( + groupId, + groupName || 'Unknown', + filteredMembers, + avatarPath + ); } } diff --git a/ts/components/dialog/UpdateGroupNameDialog.tsx b/ts/components/dialog/UpdateGroupNameDialog.tsx index e96b2e612..d69aa40f3 100644 --- a/ts/components/dialog/UpdateGroupNameDialog.tsx +++ b/ts/components/dialog/UpdateGroupNameDialog.tsx @@ -16,7 +16,7 @@ type Props = { }; interface State { - groupName: string; + groupName: string | undefined; errorDisplayed: boolean; errorMessage: string; avatar: string | null; @@ -50,7 +50,7 @@ export class UpdateGroupNameDialog extends React.Component { } public onClickOK() { - if (!this.state.groupName.trim()) { + if (!this.state.groupName?.trim()) { this.onShowError(window.i18n('emptyGroupNameError')); return; @@ -76,7 +76,7 @@ export class UpdateGroupNameDialog extends React.Component { public render() { const okText = window.i18n('ok'); const cancelText = window.i18n('cancel'); - const titleText = window.i18n('updateGroupDialogTitle', this.convo.getName()); + const titleText = window.i18n('updateGroupDialogTitle', [this.convo.getName() || 'Unknown']); const errorMsg = this.state.errorMessage; const errorMessageClasses = classNames( diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx index d812044a1..44cc6db22 100644 --- a/ts/components/session/calling/IncomingCallDialog.tsx +++ b/ts/components/session/calling/IncomingCallDialog.tsx @@ -75,7 +75,7 @@ export const IncomingCallDialog = () => { if (hasIncomingCall) { return ( - + diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index f66dfec92..a83d03db8 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -244,7 +244,7 @@ export const SessionRightPanelWithDetails = () => { <>
- {window.i18n('members', subscriberCount)} + {window.i18n('members', [`${subscriberCount}`])}
diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index f325086d9..9b05d96d5 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -35,6 +35,7 @@ import { import { SessionButtonColor } from '../SessionButton'; import { getTimerOptions } from '../../../state/selectors/timerOptions'; import { ToastUtils } from '../../../session/utils'; +import { LocalizerKeys } from '../../../types/LocalizerKeys'; const maxNumberOfPinnedConversations = 5; @@ -155,7 +156,7 @@ export const getPinConversationMenuItem = (conversationId: string): JSX.Element ToastUtils.pushToastWarning( 'pinConversationLimitToast', window.i18n('pinConversationLimitTitle'), - window.i18n('pinConversationLimitToastDescription', maxNumberOfPinnedConversations) + window.i18n('pinConversationLimitToastDescription', [`${maxNumberOfPinnedConversations}`]) ); } }; @@ -435,8 +436,14 @@ export function getNotificationForConvoMenuItem({ const notificationForConvoOptions = ConversationNotificationSetting.filter(n => isPrivate ? n !== 'mentions_only' : true ).map((n: ConversationNotificationSettingType) => { - // this link to the notificationForConvo_all, notificationForConvo_mentions_only, ... - return { value: n, name: window.i18n(`notificationForConvo_${n}`) }; + // do this separately so typescript's compiler likes it + const keyToUse: LocalizerKeys = + n === 'all' + ? 'notificationForConvo_all' + : n === 'disabled' + ? 'notificationForConvo_disabled' + : 'notificationForConvo_mentions_only'; + return { value: n, name: window.i18n(keyToUse) }; }); return ( diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx index d23918749..3e1c82efe 100644 --- a/ts/components/session/registration/SignInTab.tsx +++ b/ts/components/session/registration/SignInTab.tsx @@ -98,7 +98,7 @@ export const SignInTab = () => { const [recoveryPhrase, setRecoveryPhrase] = useState(''); const [recoveryPhraseError, setRecoveryPhraseError] = useState(undefined as string | undefined); const [displayName, setDisplayName] = useState(''); - const [displayNameError, setDisplayNameError] = useState(''); + const [displayNameError, setDisplayNameError] = useState(''); const [loading, setIsLoading] = useState(false); const isRecovery = signInMode === SignInMode.UsingRecoveryPhrase; diff --git a/ts/components/session/registration/SignUpTab.tsx b/ts/components/session/registration/SignUpTab.tsx index e6a5c08f4..dabe4db95 100644 --- a/ts/components/session/registration/SignUpTab.tsx +++ b/ts/components/session/registration/SignUpTab.tsx @@ -87,7 +87,7 @@ export const SignUpTab = () => { setSignUpMode, } = useContext(RegistrationContext); const [displayName, setDisplayName] = useState(''); - const [displayNameError, setDisplayNameError] = useState(''); + const [displayNameError, setDisplayNameError] = useState(''); useEffect(() => { if (signUpMode === SignUpMode.SessionIDShown) { diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 4501e67f4..f1a441574 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -12,6 +12,7 @@ import { SessionNotificationGroupSettings } from './SessionNotificationGroupSett import { BlockedUserSettings } from './BlockedUserSettings'; import { SettingsCategoryPrivacy } from './section/CategoryPrivacy'; import { SettingsCategoryAppearance } from './section/CategoryAppearance'; +import { LocalizerKeys } from '../../../types/LocalizerKeys'; export function getMediaPermissionsSettings() { return window.getSettingValue('media-permissions'); @@ -179,13 +180,18 @@ export class SessionSettingsView extends React.Component - +
{shouldRenderPasswordLock ? ( diff --git a/ts/interactions/messageInteractions.ts b/ts/interactions/messageInteractions.ts index 98d2ce101..59ab07def 100644 --- a/ts/interactions/messageInteractions.ts +++ b/ts/interactions/messageInteractions.ts @@ -176,8 +176,8 @@ const acceptOpenGroupInvitationV2 = (completeUrl: string, roomName?: string) => window.inboxStore?.dispatch( updateConfirmModal({ - title: window.i18n('joinOpenGroupAfterInvitationConfirmationTitle', roomName), - message: window.i18n('joinOpenGroupAfterInvitationConfirmationDesc', roomName), + title: window.i18n('joinOpenGroupAfterInvitationConfirmationTitle', [roomName || 'Unknown']), + message: window.i18n('joinOpenGroupAfterInvitationConfirmationDesc', [roomName || 'Unknown']), onClickOk: async () => { await joinOpenGroupV2WithUIEvents(completeUrl, true, false); }, diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 20307bf5b..b2209829f 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -906,7 +906,9 @@ export class ConversationModel extends Backbone.Model { currentTimestamp: this.get('active_at'), lastMessage: lastMessageJSON, lastMessageStatus: lastMessageStatusModel, - lastMessageNotificationText: lastMessageModel ? lastMessageModel.getNotificationText() : null, + lastMessageNotificationText: lastMessageModel + ? lastMessageModel.getNotificationText() + : undefined, }); this.set(lastMessageUpdate); await this.commit(); diff --git a/ts/models/message.ts b/ts/models/message.ts index b4f0c7a9e..f7f75bdee 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -175,10 +175,9 @@ export class MessageModel extends Backbone.Model { (groupUpdate.left && Array.isArray(groupUpdate.left) && groupUpdate.left.length === 1) || typeof groupUpdate.left === 'string' ) { - return window.i18n( - 'leftTheGroup', - getConversationController().getContactProfileNameOrShortenedPubKey(groupUpdate.left) - ); + return window.i18n('leftTheGroup', [ + getConversationController().getContactProfileNameOrShortenedPubKey(groupUpdate.left), + ]); } if (groupUpdate.kicked === 'You') { return window.i18n('youGotKickedFromGroup'); @@ -210,9 +209,9 @@ export class MessageModel extends Backbone.Model { ); if (names.length > 1) { - messages.push(window.i18n('multipleKickedFromTheGroup', names.join(', '))); + messages.push(window.i18n('multipleKickedFromTheGroup', [names.join(', ')])); } else { - messages.push(window.i18n('kickedFromTheGroup', names[0])); + messages.push(window.i18n('kickedFromTheGroup', [names[0]])); } } return messages.join(' '); @@ -223,21 +222,35 @@ export class MessageModel extends Backbone.Model { if (this.isGroupInvitation()) { return `😎 ${window.i18n('openGroupInvitation')}`; } + if (this.isDataExtractionNotification()) { const dataExtraction = this.get( 'dataExtractionNotification' ) as DataExtractionNotificationMsg; if (dataExtraction.type === SignalService.DataExtractionNotification.Type.SCREENSHOT) { - return window.i18n( - 'tookAScreenshot', - getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source) - ); + return window.i18n('tookAScreenshot', [ + getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source), + ]); } - return window.i18n( - 'savedTheFile', - getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source) + return window.i18n('savedTheFile', [ + getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source), + ]); + } + if (this.get('callNotificationType')) { + const displayName = getConversationController().getContactProfileNameOrShortenedPubKey( + this.get('conversationId') ); + const callNotificationType = this.get('callNotificationType'); + if (callNotificationType === 'missed-call') { + return window.i18n('callMissed', [displayName]); + } + if (callNotificationType === 'started-call') { + return window.i18n('startedACall', [displayName]); + } + if (callNotificationType === 'answered-a-call') { + return window.i18n('answeredACall', [displayName]); + } } if (this.get('callNotificationType')) { const displayName = getConversationController().getContactProfileNameOrShortenedPubKey( @@ -245,13 +258,13 @@ export class MessageModel extends Backbone.Model { ); const callNotificationType = this.get('callNotificationType'); if (callNotificationType === 'missed-call') { - return window.i18n('callMissed', displayName); + return window.i18n('callMissed', [displayName]); } if (callNotificationType === 'started-call') { - return window.i18n('startedACall', displayName); + return window.i18n('startedACall', [displayName]); } if (callNotificationType === 'answered-a-call') { - return window.i18n('answeredACall', displayName); + return window.i18n('answeredACall', [displayName]); } } return this.get('body'); @@ -276,7 +289,7 @@ export class MessageModel extends Backbone.Model { pubkey.slice(1) ); if (displayName && displayName.length) { - description = description.replace(pubkey, `@${displayName}`); + description = description?.replace(pubkey, `@${displayName}`); } }); return description; @@ -690,7 +703,7 @@ export class MessageModel extends Backbone.Model { } = { authorPhoneNumber: author, messageId: id, - authorName, + authorName: authorName || 'Unknown', }; if (referencedMessageNotFound) { diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 53cc707b8..36541e582 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -144,7 +144,7 @@ export function pushedMissedCall(conversationName: string) { pushToastInfo( 'missedCall', window.i18n('callMissedTitle'), - window.i18n('callMissed', conversationName) + window.i18n('callMissed', [conversationName]) ); } @@ -158,7 +158,7 @@ export function pushedMissedCallCauseOfPermission(conversationName: string) { toast.info( , diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 85e21a34c..9d15cc59d 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -784,7 +784,7 @@ export const getMessagePropsByMessageId = createSelector( authorPhoneNumber, authorAvatarPath: foundSenderConversation.avatarPath || null, isKickedFromGroup: foundMessageConversation.isKickedFromGroup || false, - authorProfileName, + authorProfileName: authorProfileName || 'Unknown', authorName, }, }; diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts new file mode 100644 index 000000000..75ff81dd3 --- /dev/null +++ b/ts/types/LocalizerKeys.ts @@ -0,0 +1,465 @@ +export type LocalizerKeys = + | 'gotIt' + | 'removePassword' + | 'editMenuDelete' + | 'signIn' + | 'newClosedGroup' + | 'userUnbanFailed' + | 'changePassword' + | 'saved' + | 'startedACall' + | 'mainMenuWindow' + | 'unblocked' + | 'userAddedToModerators' + | 'to' + | 'sent' + | 'requestsPlaceholder' + | 'closedGroupInviteFailMessage' + | 'noContactsForGroup' + | 'originalMessageNotAvailable' + | 'linkVisitWarningMessage' + | 'editMenuPasteAndMatchStyle' + | 'anonymous' + | 'viewMenuZoomOut' + | 'dialogClearAllDataDeletionFailedDesc' + | 'timerOption_10_seconds_abbreviated' + | 'enterDisplayName' + | 'connectToServerFail' + | 'disableNotifications' + | 'publicChatExists' + | 'passwordViewTitle' + | 'joinOpenGroupAfterInvitationConfirmationTitle' + | 'notificationMostRecentFrom' + | 'timerOption_5_minutes' + | 'linkPreviewsConfirmMessage' + | 'notificationMostRecent' + | 'video' + | 'readReceiptSettingDescription' + | 'userBanFailed' + | 'autoUpdateLaterButtonLabel' + | 'maximumAttachments' + | 'deviceOnly' + | 'expiredWarning' + | 'beginYourSession' + | 'typingIndicatorsSettingDescription' + | 'changePasswordToastDescription' + | 'addingContacts' + | 'passwordLengthError' + | 'typingIndicatorsSettingTitle' + | 'maxPasswordAttempts' + | 'viewMenuToggleDevTools' + | 'fileSizeWarning' + | 'openGroupURL' + | 'messageRequestsDescription' + | 'hideMenuBarDescription' + | 'search' + | 'pickClosedGroupMember' + | 'ByUsingThisService...' + | 'startConversation' + | 'unableToCallTitle' + | 'yourUniqueSessionID' + | 'typingAlt' + | 'orJoinOneOfThese' + | 'members' + | 'sendRecoveryPhraseMessage' + | 'timerOption_1_hour' + | 'youGotKickedFromGroup' + | 'cannotRemoveCreatorFromGroupDesc' + | 'incomingError' + | 'notificationsSettingsTitle' + | 'ringing' + | 'tookAScreenshot' + | 'from' + | 'thisMonth' + | 'next' + | 'addModerators' + | 'sessionMessenger' + | 'today' + | 'appMenuHideOthers' + | 'sendFailed' + | 'enterPassword' + | 'me' + | 'enterSessionIDOfRecipient' + | 'dialogClearAllDataDeletionFailedMultiple' + | 'pinConversationLimitToastDescription' + | 'appMenuQuit' + | 'windowMenuZoom' + | 'allUsersAreRandomly...' + | 'cameraPermissionNeeded' + | 'requestsSubtitle' + | 'closedGroupInviteSuccessTitle' + | 'accept' + | 'setPasswordTitle' + | 'editMenuUndo' + | 'pinConversation' + | 'lightboxImageAlt' + | 'linkDevice' + | 'goToOurSurvey' + | 'invalidPubkeyFormat' + | 'disappearingMessagesDisabled' + | 'spellCheckDescription' + | 'autoUpdateNewVersionInstructions' + | 'appMenuUnhide' + | 'timerOption_30_minutes_abbreviated' + | 'description' + | 'voiceMessage' + | 'changePasswordTitle' + | 'copyMessage' + | 'messageDeletionForbidden' + | 'deleteJustForMe' + | 'changeAccountPasswordTitle' + | 'onionPathIndicatorDescription' + | 'timestamp_s' + | 'mediaPermissionsTitle' + | 'replyingToMessage' + | 'welcomeToYourSession' + | 'editMenuCopy' + | 'timestamp_m' + | 'leftTheGroup' + | 'timerOption_30_minutes' + | 'nameOnly' + | 'typeInOldPassword' + | 'imageAttachmentAlt' + | 'displayNameEmpty' + | 'inviteContacts' + | 'callMediaPermissionsTitle' + | 'blocked' + | 'noBlockedContacts' + | 'leaveGroupConfirmation' + | 'banUserConfirm' + | 'banUserAndDeleteAll' + | 'joinOpenGroupAfterInvitationConfirmationDesc' + | 'invalidNumberError' + | 'newSession' + | 'contextMenuNoSuggestions' + | 'recoveryPhraseRevealButtonText' + | 'banUser' + | 'permissions' + | 'answeredACall' + | 'sendMessage' + | 'recoveryPhraseRevealMessage' + | 'showRecoveryPhrase' + | 'autoUpdateSettingDescription' + | 'unlock' + | 'remove' + | 'restoreUsingRecoveryPhrase' + | 'cannotUpdateDetail' + | 'showRecoveryPhrasePasswordRequest' + | 'spellCheckDirty' + | 'debugLogExplanation' + | 'closedGroupInviteFailTitle' + | 'setAccountPasswordDescription' + | 'removeAccountPasswordDescription' + | 'establishingConnection' + | 'noModeratorsToRemove' + | 'moreInformation' + | 'offline' + | 'appearanceSettingsTitle' + | 'mainMenuView' + | 'mainMenuEdit' + | 'notificationForConvo_disabled' + | 'leaveGroupConfirmationAdmin' + | 'notificationForConvo_all' + | 'emptyGroupNameError' + | 'copyOpenGroupURL' + | 'setPasswordInvalid' + | 'timerOption_30_seconds_abbreviated' + | 'removeResidueMembers' + | 'timerOption_1_hour_abbreviated' + | 'areYouSureDeleteEntireAccount' + | 'noGivenPassword' + | 'closedGroupInviteOkText' + | 'readReceiptSettingTitle' + | 'copySessionID' + | 'timerOption_0_seconds' + | 'zoomFactorSettingTitle' + | 'unableToCall' + | 'callMissedTitle' + | 'done' + | 'videoAttachmentAlt' + | 'message' + | 'mainMenuHelp' + | 'open' + | 'pasteLongPasswordToastTitle' + | 'nameAndMessage' + | 'autoUpdateDownloadedMessage' + | 'onionPathIndicatorTitle' + | 'unknown' + | 'submitDebugLog' + | 'mediaMessage' + | 'addAsModerator' + | 'closedGroupInviteFailTitlePlural' + | 'enterSessionID' + | 'editGroup' + | 'incomingCallFrom' + | 'timerSetOnSync' + | 'deleteMessages' + | 'editMenuSelectAll' + | 'spellCheckTitle' + | 'translation' + | 'copy' + | 'messageBodyMissing' + | 'timerOption_12_hours_abbreviated' + | 'onlyAdminCanRemoveMembersDesc' + | 'recording' + | 'kickedFromTheGroup' + | 'windowMenuMinimize' + | 'debugLog' + | 'timerOption_0_seconds_abbreviated' + | 'timerOption_5_minutes_abbreviated' + | 'enterOptionalPassword' + | 'goToReleaseNotes' + | 'unpinConversation' + | 'viewMenuResetZoom' + | 'startInTrayDescription' + | 'groupNamePlaceholder' + | 'stagedPreviewThumbnail' + | 'helpUsTranslateSession' + | 'unreadMessages' + | 'documents' + | 'audioPermissionNeededTitle' + | 'deleteMessagesQuestion' + | 'clickToTrustContact' + | 'closedGroupInviteFailMessagePlural' + | 'noAudioInputFound' + | 'timerOption_10_seconds' + | 'noteToSelf' + | 'failedToAddAsModerator' + | 'disabledDisappearingMessages' + | 'cannotUpdate' + | 'device' + | 'replyToMessage' + | 'messageDeletedPlaceholder' + | 'notificationFrom' + | 'displayName' + | 'invalidSessionId' + | 'audioPermissionNeeded' + | 'timestamp_h' + | 'add' + | 'windowMenuBringAllToFront' + | 'messageRequests' + | 'show' + | 'cannotMixImageAndNonImageAttachments' + | 'viewMenuToggleFullScreen' + | 'optimizingApplication' + | 'goToSupportPage' + | 'passwordsDoNotMatch' + | 'createClosedGroupNamePrompt' + | 'upgrade' + | 'audioMessageAutoplayDescription' + | 'leaveAndRemoveForEveryone' + | 'previewThumbnail' + | 'photo' + | 'setPassword' + | 'hideMenuBarTitle' + | 'imageCaptionIconAlt' + | 'blockAll' + | 'sendRecoveryPhraseTitle' + | 'multipleJoinedTheGroup' + | 'databaseError' + | 'resend' + | 'copiedToClipboard' + | 'closedGroupInviteSuccessTitlePlural' + | 'groupMembers' + | 'dialogClearAllDataDeletionQuestion' + | 'unableToLoadAttachment' + | 'cameraPermissionNeededTitle' + | 'editMenuRedo' + | 'view' + | 'changeNicknameMessage' + | 'close' + | 'deleteMessageQuestion' + | 'newMessage' + | 'windowMenuClose' + | 'mainMenuFile' + | 'callMissed' + | 'getStarted' + | 'unblockUser' + | 'blockUser' + | 'trustThisContactDialogTitle' + | 'received' + | 'privacyPolicy' + | 'setPasswordFail' + | 'clearNickname' + | 'connectToServerSuccess' + | 'viewMenuZoomIn' + | 'invalidOpenGroupUrl' + | 'entireAccount' + | 'noContactsToAdd' + | 'cancel' + | 'decline' + | 'originalMessageNotFound' + | 'autoUpdateRestartButtonLabel' + | 'deleteConversationConfirmation' + | 'unreadMessage' + | 'timerOption_6_hours_abbreviated' + | 'timerOption_1_week_abbreviated' + | 'timerSetTo' + | 'unbanUserConfirm' + | 'notificationSubtitle' + | 'youChangedTheTimer' + | 'updatedTheGroup' + | 'leaveGroup' + | 'menuReportIssue' + | 'continueYourSession' + | 'invalidGroupNameTooShort' + | 'notificationForConvo' + | 'noNameOrMessage' + | 'pinConversationLimitTitle' + | 'noSearchResults' + | 'changeNickname' + | 'userUnbanned' + | 'error' + | 'clearAllData' + | 'contactAvatarAlt' + | 'disappearingMessages' + | 'autoUpdateNewVersionTitle' + | 'linkPreviewDescription' + | 'timerOption_1_day' + | 'contactsHeader' + | 'openGroupInvitation' + | 'callMissedCausePermission' + | 'messageFoundButNotLoaded' + | 'mediaPermissionsDescription' + | 'media' + | 'noMembersInThisGroup' + | 'saveLogToDesktop' + | 'copyErrorAndQuit' + | 'speech' + | 'onlyAdminCanRemoveMembers' + | 'passwordTypeError' + | 'createClosedGroupPlaceholder' + | 'editProfileModalTitle' + | 'noCameraFound' + | 'setAccountPasswordTitle' + | 'callMediaPermissionsDescription' + | 'recoveryPhraseSecureTitle' + | 'yesterday' + | 'closedGroupInviteSuccessMessage' + | 'youDisabledDisappearingMessages' + | 'updateGroupDialogTitle' + | 'surveyTitle' + | 'userRemovedFromModerators' + | 'timerOption_5_seconds' + | 'failedToRemoveFromModerator' + | 'conversationsHeader' + | 'setPasswordToastDescription' + | 'audio' + | 'startInTrayTitle' + | 'cannotRemoveCreatorFromGroup' + | 'editMenuCut' + | 'markAllAsRead' + | 'failedResolveOns' + | 'showDebugLog' + | 'autoUpdateDownloadButtonLabel' + | 'dialogClearAllDataDeletionFailedTitleQuestion' + | 'autoUpdateDownloadInstructions' + | 'dialogClearAllDataDeletionFailedTitle' + | 'loading' + | 'blockedSettingsTitle' + | 'checkNetworkConnection' + | 'appMenuHide' + | 'removeAccountPasswordTitle' + | 'recoveryPhraseEmpty' + | 'noAudioOutputFound' + | 'save' + | 'privacySettingsTitle' + | 'changeAccountPasswordDescription' + | 'notificationSettingsDialog' + | 'invalidOldPassword' + | 'audioMessageAutoplayTitle' + | 'removePasswordInvalid' + | 'password' + | 'usersCanShareTheir...' + | 'timestampFormat_M' + | 'banUserAndDeleteAllConfirm' + | 'nicknamePlaceholder' + | 'linkPreviewsTitle' + | 'continue' + | 'learnMore' + | 'successUnlinked' + | 'autoUpdateSettingTitle' + | 'deleteForEveryone' + | 'createSessionID' + | 'multipleLeftTheGroup' + | 'enterSessionIDOrONSName' + | 'quoteThumbnailAlt' + | 'timerOption_1_week' + | 'deleteContactConfirmation' + | 'timerOption_30_seconds' + | 'createAccount' + | 'timerOption_1_minute_abbreviated' + | 'dangerousFileType' + | 'timerOption_12_hours' + | 'unblockToSend' + | 'timerOption_1_minute' + | 'yourSessionID' + | 'deleteAccountWarning' + | 'deleted' + | 'closedGroupMaxSize' + | 'messagesHeader' + | 'passwordCharacterError' + | 'joinOpenGroup' + | 'callMediaPermissionsDialogContent' + | 'timerOption_1_day_abbreviated' + | 'about' + | 'ok' + | 'multipleKickedFromTheGroup' + | 'recoveryPhraseSavePromptMain' + | 'editMenuPaste' + | 'areYouSureDeleteDeviceOnly' + | 'or' + | 'removeModerators' + | 'destination' + | 'invalidGroupNameTooLong' + | 'youLeftTheGroup' + | 'theyChangedTheTimer' + | 'userBanned' + | 'addACaption' + | 'debugLogError' + | 'timerOption_5_seconds_abbreviated' + | 'removeFromModerators' + | 'enterRecoveryPhrase' + | 'submit' + | 'stagedImageAttachment' + | 'thisWeek' + | 'savedTheFile' + | 'mediaEmptyState' + | 'linkVisitWarningTitle' + | 'invalidPassword' + | 'endCall' + | 'latestUnreadIsAbove' + | 'connectingToServer' + | 'notifications' + | 'settingsHeader' + | 'autoUpdateNewVersionMessage' + | 'oneNonImageAtATimeToast' + | 'menuCall' + | 'attemptingReconnection' + | 'removePasswordTitle' + | 'iAmSure' + | 'selectMessage' + | 'enterAnOpenGroupURL' + | 'delete' + | 'changePasswordInvalid' + | 'unblockGroupToSend' + | 'general' + | 'timerOption_6_hours' + | 'confirmPassword' + | 'downloadAttachment' + | 'showUserDetails' + | 'titleIsNow' + | 'removePasswordToastDescription' + | 'recoveryPhrase' + | 'newMessages' + | 'you' + | 'documentsEmptyState' + | 'unbanUser' + | 'permissionSettingsTitle' + | 'notificationForConvo_mentions_only' + | 'trustThisContactDialogDescription' + | 'unknownCountry' + | 'searchFor...' + | 'joinedTheGroup' + | 'editGroupName' + | 'reportIssue'; diff --git a/ts/types/Util.ts b/ts/types/Util.ts index bf9cd05de..ad27b4554 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -1,3 +1,5 @@ +import { LocalizerKeys } from './LocalizerKeys'; + export type RenderTextCallbackType = (options: { text: string; key: number; @@ -5,7 +7,7 @@ export type RenderTextCallbackType = (options: { convoId?: string; }) => JSX.Element | string; -export type LocalizerType = (key: string, values?: Array) => string; +export type LocalizerType = (key: LocalizerKeys, values?: Array) => string; export type ColorType = | 'gray' diff --git a/ts/window.d.ts b/ts/window.d.ts index 389e29876..c86199e5c 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -1,9 +1,8 @@ import {} from 'styled-components/cssprop'; -import { LocalizerType } from '../types/Util'; +import { LocalizerType } from '../ts/types/Util'; import { LibsignalProtocol } from '../../libtextsecure/libsignal-protocol'; import { SignalInterface } from '../../js/modules/signal'; - import { LibTextsecure } from '../libtextsecure'; import { Store } from 'redux'; From 1ec637b551eb845d16c27e3d1d5b6e7a2105fb85 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 30 Nov 2021 19:14:18 -0400 Subject: [PATCH 56/70] Open group regex fixes (#2058) * Open group URL regex fixes - Capital letters in room tokens were not being accepted (it eventually gets lower-cased internally, which works fine, but that happens *after* the URL is tested for acceptability). - `-` in room was not being allowed (it is and always has been on SOGS, session-android, and session-ios). - single-letter room ids are valid, but only 2+ letter ids were being accepted. - complete URL regex wasn't anchored so something like `garbagehttps://example.com/room?public_key=<64hex>moregarbage` was being accepted in the GUI input (it fails later when other code tries to parse it as a URL). - removed `m` modifier from open group regex: without anchors it wasn't doing anything anyway, but *with* anchors it would still allow leading/trailing garbage if delineated by newlines. - public key regex was accepting g-z letters, and not accepting A-F. - various regex cleanups: - use non-capture groups (?:...) rather than capturing groups (...) - avoid repetition in host segment matching - tightened up host pattern matching a bit: - DNS host segments have a max length of 63 - Limit port max length to 5, and disallow starting with 0 * Show an error when the open group URL is invalid It's quite disconcerting when you have a bad open group URL and try to add it and the join button just "doesn't work" without any feedback at all. Fix it to show an error message. (There is already an i18n entry for this because this same message is thrown if the URL can't be parsed later on). --- .../session/LeftPaneMessageSection.tsx | 1 + ts/opengroup/utils/OpenGroupUtils.ts | 20 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 79d7874b3..5407b0490 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -461,6 +461,7 @@ export class LeftPaneMessageSection extends React.Component { this.handleToggleOverlay(undefined); } } else { + ToastUtils.pushToastError('invalidOpenGroupUrl', window.i18n('invalidOpenGroupUrl')); window.log.warn('Invalid opengroupv2 url'); } } diff --git a/ts/opengroup/utils/OpenGroupUtils.ts b/ts/opengroup/utils/OpenGroupUtils.ts index a71cf8256..d72048daf 100644 --- a/ts/opengroup/utils/OpenGroupUtils.ts +++ b/ts/opengroup/utils/OpenGroupUtils.ts @@ -1,21 +1,20 @@ import { OpenGroupV2Room } from '../../data/opengroups'; import { OpenGroupRequestCommonType } from '../opengroupV2/ApiUtil'; -const protocolRegex = new RegExp('(https?://)?'); +const protocolRegex = new RegExp('https?://'); const dot = '\\.'; const qMark = '\\?'; -const hostnameRegex = new RegExp( - `(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])${dot})*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])` -); -const portRegex = '(:[0-9]+)?'; +const hostSegment = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; +const hostnameRegex = new RegExp(`(?:${hostSegment}${dot})+${hostSegment}`); +const portRegex = ':[1-9][0-9]{0,4}'; -// roomIds allows between 2 and 64 of '0-9' or 'a-z' or '_' chars -export const roomIdV2Regex = '[0-9a-z_]{2,64}'; -export const publicKeyRegex = '[0-9a-z]{64}'; +// roomIds allow up to 64 ascii numbers, letters, '_', or '-' chars +export const roomIdV2Regex = '[0-9a-zA-Z_-]{1,64}'; +export const publicKeyRegex = '[0-9a-fA-F]{64}'; export const publicKeyParam = 'public_key='; export const openGroupV2ServerUrlRegex = new RegExp( - `${protocolRegex.source}${hostnameRegex.source}${portRegex}` + `(?:${protocolRegex.source})?${hostnameRegex.source}(?:${portRegex})?` ); /** @@ -25,8 +24,7 @@ export const openGroupV2ServerUrlRegex = new RegExp( * see https://stackoverflow.com/a/9275499/1680951 */ export const openGroupV2CompleteURLRegex = new RegExp( - `${openGroupV2ServerUrlRegex.source}\/${roomIdV2Regex}${qMark}${publicKeyParam}${publicKeyRegex}`, - 'm' + `^${openGroupV2ServerUrlRegex.source}\/${roomIdV2Regex}${qMark}${publicKeyParam}${publicKeyRegex}$` ); /** From 1a699879cf7931af632a43ac73baec763cd26067 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 2 Dec 2021 11:13:47 +1100 Subject: [PATCH 57/70] Add call duration (#2059) * add call duration once connected * close incoming call dialog if endCall from same sender * disable message request toggle if featureFlag is OFF --- js/background.js | 2 +- main.js | 6 +-- preload.js | 18 ------- ts/components/conversation/Timestamp.tsx | 2 +- .../session/SessionClosableOverlay.tsx | 2 +- .../calling/InConversationCallContainer.tsx | 44 +++++++++++++---- .../settings/section/CategoryPrivacy.tsx | 21 +++++---- ts/session/utils/calling/CallManager.ts | 24 +++++++++- ts/state/selectors/call.ts | 47 ++++++++++--------- 9 files changed, 102 insertions(+), 64 deletions(-) diff --git a/js/background.js b/js/background.js index bbf003ee3..5fae82823 100644 --- a/js/background.js +++ b/js/background.js @@ -427,7 +427,7 @@ Whisper.Notifications.disable(); // avoid notification flood until empty setTimeout(() => { Whisper.Notifications.enable(); - }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); + }, 10 * 1000); // 10 sec window.NewReceiver.queueAllCached(); window.libsession.Utils.AttachmentDownloads.start({ diff --git a/main.js b/main.js index c0cdc9c5f..307069835 100644 --- a/main.js +++ b/main.js @@ -246,7 +246,7 @@ async function createWindow() { minWidth, minHeight, autoHideMenuBar: false, - backgroundColor: '#fff', + backgroundColor: '#000', webPreferences: { nodeIntegration: false, enableRemoteModule: true, @@ -535,7 +535,7 @@ function showAbout() { resizable: false, title: locale.messages.about, autoHideMenuBar: true, - backgroundColor: '#ffffff', + backgroundColor: '#000', show: false, webPreferences: { nodeIntegration: false, @@ -577,7 +577,7 @@ async function showDebugLogWindow() { resizable: false, title: locale.messages.debugLog, autoHideMenuBar: true, - backgroundColor: '#FFFFFF', + backgroundColor: '#000', show: false, modal: true, webPreferences: { diff --git a/preload.js b/preload.js index cd6fa895e..e53586ed7 100644 --- a/preload.js +++ b/preload.js @@ -55,12 +55,6 @@ window.isBeforeVersion = (toCheck, baseVersion) => { } }; -// eslint-disable-next-line func-names -window.CONSTANTS = new (function() { - // Number of seconds to turn on notifications after reconnect/start of app - this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10; -})(); - window.versionInfo = { environment: window.getEnvironment(), version: window.getVersion(), @@ -270,9 +264,7 @@ window.moment.updateLocale(localeSetForMoment, { }); window.libsession = require('./ts/session'); -window.models = require('./ts/models'); -window.Signal = window.Signal || {}; window.Signal.Data = require('./ts/data/data'); window.Signal.Logs = require('./js/modules/logs'); @@ -287,16 +279,6 @@ window.addEventListener('contextmenu', e => { }); window.NewReceiver = require('./ts/receiver/receiver'); -window.Fsv2 = require('./ts/fileserver/FileServerApiV2'); -window.DataMessageReceiver = require('./ts/receiver/dataMessage'); -window.NewSnodeAPI = require('./ts/session/snode_api/SNodeAPI'); -window.SnodePool = require('./ts/session/snode_api/snodePool'); - -// eslint-disable-next-line no-extend-native,func-names -Promise.prototype.ignore = function() { - // eslint-disable-next-line more/no-then - this.then(() => {}); -}; // Blocking diff --git a/ts/components/conversation/Timestamp.tsx b/ts/components/conversation/Timestamp.tsx index e5d61204e..e277062f8 100644 --- a/ts/components/conversation/Timestamp.tsx +++ b/ts/components/conversation/Timestamp.tsx @@ -55,7 +55,7 @@ export const Timestamp = (props: Props) => { // Use relative time for under 24hrs ago. const now = Math.floor(Date.now()); - const messageAgeInDays = (now - timestamp) / (window.CONSTANTS.SECS_IN_DAY * 1000); + const messageAgeInDays = (now - timestamp) / (1000 * 60 * 60 * 24); const daysBeforeRelativeTiming = 1; let dateString; diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index eddc9450f..1c280a2c9 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -177,7 +177,7 @@ export class SessionClosableOverlay extends React.Component { placeholder={placeholder} value={groupName} isGroup={true} - maxLength={window.CONSTANTS.MAX_GROUPNAME_LENGTH} + maxLength={100} onChange={this.onGroupNameChanged} onPressEnter={() => onButtonClick(groupName, selectedMembers)} /> diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index e0b5f1d67..5069d01b3 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -1,14 +1,15 @@ -import React, { useRef } from 'react'; import { useSelector } from 'react-redux'; +import React, { useRef, useState } from 'react'; import styled from 'styled-components'; import _ from 'underscore'; -import { UserUtils } from '../../../session/utils'; +import { CallManager, UserUtils } from '../../../session/utils'; import { getCallIsInFullScreen, + getCallWithFocusedConvoIsOffering, + getCallWithFocusedConvosIsConnected, + getCallWithFocusedConvosIsConnecting, getHasOngoingCallWithFocusedConvo, - getHasOngoingCallWithFocusedConvoIsOffering, - getHasOngoingCallWithFocusedConvosIsConnecting, getHasOngoingCallWithPubkey, } from '../../../state/selectors/call'; import { StyledVideoElement } from './DraggableCallContainer'; @@ -19,11 +20,15 @@ import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots' import { CallWindowControls } from './CallButtons'; import { SessionSpinner } from '../SessionSpinner'; import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/calling/CallManager'; +// tslint:disable-next-line: no-submodule-imports +import useInterval from 'react-use/lib/useInterval'; +import moment from 'moment'; const VideoContainer = styled.div` height: 100%; width: 50%; z-index: 0; + padding-top: 30px; // leave some space at the top for the connecting/duration of the current call `; const InConvoCallWindow = styled.div` @@ -66,10 +71,11 @@ const StyledCenteredLabel = styled.div` white-space: nowrap; color: white; text-shadow: 0px 0px 8px white; + z-index: 5; `; const RingingLabel = () => { - const ongoingCallWithFocusedIsRinging = useSelector(getHasOngoingCallWithFocusedConvoIsOffering); + const ongoingCallWithFocusedIsRinging = useSelector(getCallWithFocusedConvoIsOffering); const modulatedStr = useModuloWithTripleDots(window.i18n('ringing'), 3, 1000); if (!ongoingCallWithFocusedIsRinging) { @@ -79,9 +85,7 @@ const RingingLabel = () => { }; const ConnectingLabel = () => { - const ongoingCallWithFocusedIsConnecting = useSelector( - getHasOngoingCallWithFocusedConvosIsConnecting - ); + const ongoingCallWithFocusedIsConnecting = useSelector(getCallWithFocusedConvosIsConnecting); const modulatedStr = useModuloWithTripleDots(window.i18n('establishingConnection'), 3, 1000); @@ -92,6 +96,29 @@ const ConnectingLabel = () => { return {modulatedStr}; }; +const DurationLabel = () => { + const [callDuration, setCallDuration] = useState(undefined); + const ongoingCallWithFocusedIsConnected = useSelector(getCallWithFocusedConvosIsConnected); + + useInterval(() => { + const duration = CallManager.getCurrentCallDuration(); + if (duration) { + setCallDuration(duration); + } + }, 100); + + if (!ongoingCallWithFocusedIsConnected || !callDuration || callDuration < 0) { + return null; + } + + const ms = callDuration * 1000; + const d = moment.duration(ms); + + // tslint:disable-next-line: restrict-plus-operands + const dateString = Math.floor(d.asHours()) + moment.utc(ms).format(':mm:ss'); + return {dateString}; +}; + const StyledSpinner = styled.div<{ fullWidth: boolean }>` height: 100%; width: ${props => (props.fullWidth ? '100%' : '50%')}; @@ -167,6 +194,7 @@ export const InConversationCallContainer = () => { + @@ -71,7 +73,6 @@ export const SettingsCategoryPrivacy = (props: { description={window.i18n('mediaPermissionsDescription')} active={Boolean(window.getSettingValue('media-permissions'))} /> - {window.lokiFeatureFlags.useCallMessage && ( { @@ -113,14 +114,16 @@ export const SettingsCategoryPrivacy = (props: { description={window.i18n('autoUpdateSettingDescription')} active={Boolean(window.getSettingValue(settingsAutoUpdate))} /> - { - dispatch(toggleMessageRequests()); - }} - title={window.i18n('messageRequests')} - description={window.i18n('messageRequestsDescription')} - active={useSelector(getIsMessageRequestsEnabled)} - /> + {hasMessageRequestFlag && ( + { + dispatch(toggleMessageRequests()); + }} + title={window.i18n('messageRequests')} + description={window.i18n('messageRequestsDescription')} + active={useSelector(getIsMessageRequestsEnabled)} + /> + )} {!props.hasPassword && ( = new Set(); export type CallManagerOptionsType = { @@ -591,12 +593,16 @@ function handleConnectionStateChanged(pubkey: string) { if (firstAudioOutput) { void selectAudioOutputByDeviceId(firstAudioOutput); } + + currentCallStartTimestamp = Date.now(); + window.inboxStore?.dispatch(callConnected({ pubkey })); } } function closeVideoCall() { window.log.info('closingVideoCall '); + currentCallStartTimestamp = undefined; setIsRinging(false); if (peerConnection) { peerConnection.ontrack = null; @@ -909,13 +915,13 @@ export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: stri if (aboutCallUUID) { rejectedCallUUIDS.add(aboutCallUUID); + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); // this is a end call from ourself. We must remove the popup about the incoming call // if it matches the owner of this callUUID if (sender === UserUtils.getOurPubKeyStrFromCache()) { - const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); const ownerOfCall = getOwnerOfCallUUID(aboutCallUUID); if ( @@ -928,8 +934,17 @@ export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: stri return; } + // remote user hangup while we were on the call with him if (aboutCallUUID === currentCallUUID) { closeVideoCall(); + window.inboxStore?.dispatch(endCall()); + } else if ( + ongoingCallWith === sender && + (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') + ) { + // remote user hangup an offer he sent but we did not accept it yet + setIsRinging(false); + window.inboxStore?.dispatch(endCall()); } } @@ -1295,8 +1310,15 @@ export function onTurnedOnCallMediaPermissions() { Date.now() - msg.timestamp < DURATION.MINUTES * 1 ) { window.inboxStore?.dispatch(incomingCall({ pubkey: key })); + break; } } }); }); } + +export function getCurrentCallDuration() { + return currentCallStartTimestamp + ? Math.floor((Date.now() - currentCallStartTimestamp) / 1000) + : undefined; +} diff --git a/ts/state/selectors/call.ts b/ts/state/selectors/call.ts index 819938245..788f4f6e1 100644 --- a/ts/state/selectors/call.ts +++ b/ts/state/selectors/call.ts @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { CallStateType } from '../ducks/call'; +import { CallStateType, CallStatusEnum } from '../ducks/call'; import { ConversationsStateType, ReduxConversationType } from '../ducks/conversations'; import { StateType } from '../reducer'; import { getConversations, getSelectedConversationKey } from './conversations'; @@ -55,37 +55,40 @@ export const getHasOngoingCallWithFocusedConvo = createSelector( } ); -export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector( +const getCallStateWithFocusedConvo = createSelector( getCallState, getSelectedConversationKey, - (callState: CallStateType, selectedConvoPubkey?: string): boolean => { + (callState: CallStateType, selectedConvoPubkey?: string): CallStatusEnum => { if ( - !selectedConvoPubkey || - !callState.ongoingWith || - callState.ongoingCallStatus !== 'offering' || - selectedConvoPubkey !== callState.ongoingWith + selectedConvoPubkey && + callState.ongoingWith && + selectedConvoPubkey === callState.ongoingWith ) { - return false; + return callState.ongoingCallStatus; } - return true; + return undefined; } ); -export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector( - getCallState, - getSelectedConversationKey, - (callState: CallStateType, selectedConvoPubkey?: string): boolean => { - if ( - !selectedConvoPubkey || - !callState.ongoingWith || - callState.ongoingCallStatus !== 'connecting' || - selectedConvoPubkey !== callState.ongoingWith - ) { - return false; - } +export const getCallWithFocusedConvoIsOffering = createSelector( + getCallStateWithFocusedConvo, + (callState: CallStatusEnum): boolean => { + return callState === 'offering'; + } +); + +export const getCallWithFocusedConvosIsConnecting = createSelector( + getCallStateWithFocusedConvo, + (callState: CallStatusEnum): boolean => { + return callState === 'connecting'; + } +); - return true; +export const getCallWithFocusedConvosIsConnected = createSelector( + getCallStateWithFocusedConvo, + (callState: CallStatusEnum): boolean => { + return callState === 'ongoing'; } ); From 273d866b98be07a5f47de0362c742649ae80559d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 2 Dec 2021 16:22:14 +1100 Subject: [PATCH 58/70] Cleanup message request (#2063) * close incoming call dialog if endCall from seame sender * disable message request toggle if featureFlag is OFF * cleanup UI of message requests * mark all existing conversations as approved in a migration * fix regex with conversationID for opengroups --- app/sql.js | 19 ++++ stylesheets/_session_left_pane.scss | 8 -- ts/components/Avatar.tsx | 1 + ts/components/ConversationListItem.tsx | 107 +++++++++++------- .../session/SessionClosableOverlay.tsx | 11 +- ts/opengroup/utils/OpenGroupUtils.ts | 2 +- .../conversations/ConversationController.ts | 12 +- 7 files changed, 98 insertions(+), 62 deletions(-) diff --git a/app/sql.js b/app/sql.js index 5125442f3..0948d915b 100644 --- a/app/sql.js +++ b/app/sql.js @@ -834,6 +834,7 @@ const LOKI_SCHEMA_VERSIONS = [ updateToLokiSchemaVersion14, updateToLokiSchemaVersion15, updateToLokiSchemaVersion16, + updateToLokiSchemaVersion17, ]; function updateToLokiSchemaVersion1(currentVersion, db) { @@ -1227,6 +1228,24 @@ function updateToLokiSchemaVersion16(currentVersion, db) { console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); } +function updateToLokiSchemaVersion17(currentVersion, db) { + const targetVersion = 17; + if (currentVersion >= targetVersion) { + return; + } + console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`); + + db.transaction(() => { + db.exec(` + UPDATE ${CONVERSATIONS_TABLE} SET + json = json_set(json, '$.isApproved', 1) + `); + + writeLokiSchemaVersion(targetVersion, db); + })(); + console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); +} + function writeLokiSchemaVersion(newVersion, db) { db.prepare( `INSERT INTO loki_schema( diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index f33fba1df..ade13d4d4 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -249,14 +249,6 @@ $session-compose-margin: 20px; margin-bottom: 3rem; flex-shrink: 0; } - - .message-request-list__container { - width: 100%; - - .session-button { - margin: $session-margin-xs $session-margin-xs $session-margin-xs 0; - } - } } } .module-search-results { diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index dc6d191df..4af7040f9 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -113,6 +113,7 @@ const AvatarInner = (props: Props) => { )} onClick={e => { e.stopPropagation(); + e.preventDefault(); props.onAvatarClick?.(); }} role="button" diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index f1c56e0a1..01dece988 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -25,7 +25,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; -import { Flex } from './basic/Flex'; import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { updateUserDetailsModal } from '../state/ducks/modalDialog'; import { approveConversation, blockConvoById } from '../interactions/conversationInteractions'; @@ -170,6 +169,8 @@ const MessageItem = (props: { lastMessage?: LastMessageType; isTyping: boolean; unreadCount: number; + isMessageRequest: boolean; + conversationId: string; }) => { const { lastMessage, isTyping, unreadCount } = props; @@ -196,7 +197,11 @@ const MessageItem = (props: { )}
- {lastMessage && lastMessage.status ? ( + + {lastMessage && lastMessage.status && !props.isMessageRequest ? ( ) : null}
@@ -230,6 +235,64 @@ const AvatarItem = (props: { conversationId: string; isPrivate: boolean }) => { ); }; +const RejectMessageRequestButton = ({ conversationId }: { conversationId: string }) => { + /** + * Removes conversation from requests list, + * adds ID to block list, syncs the block with linked devices. + */ + const handleConversationBlock = async () => { + await blockConvoById(conversationId); + await forceSyncConfigurationNowIfNeeded(); + }; + return ( + + ); +}; + +const ApproveMessageRequestButton = ({ conversationId }: { conversationId: string }) => { + return ( + { + await approveConversation(conversationId); + }} + backgroundColor="var(--color-accent)" + iconColor="var(--color-foreground-primary)" + iconPadding="var(--margins-xs)" + borderRadius="2px" + /> + ); +}; + +const MessageRequestButtons = ({ + conversationId, + isMessageRequest, +}: { + conversationId: string; + isMessageRequest: boolean; +}) => { + if (!isMessageRequest) { + return null; + } + + return ( + <> + + + + ); +}; + // tslint:disable: max-func-body-length const ConversationListItem = (props: Props) => { const { @@ -267,15 +330,6 @@ const ConversationListItem = (props: Props) => { [conversationId] ); - /** - * Removes conversation from requests list, - * adds ID to block list, syncs the block with linked devices. - */ - const handleConversationBlock = async () => { - await blockConvoById(conversationId); - await forceSyncConfigurationNowIfNeeded(); - }; - return (
{ isTyping={!!isTyping} unreadCount={unreadCount || 0} lastMessage={lastMessage} + isMessageRequest={Boolean(isMessageRequest)} + conversationId={conversationId} /> - {isMessageRequest ? ( - - - { - await approveConversation(conversationId); - }} - backgroundColor="var(--color-accent)" - iconColor="var(--color-foreground-primary)" - iconPadding="var(--margins-xs)" - borderRadius="2px" - /> - - ) : null}
diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 1c280a2c9..897be4bb7 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -12,6 +12,7 @@ import { SpacerLG, SpacerMD } from '../basic/Text'; import { useSelector } from 'react-redux'; import { getConversationRequests } from '../../state/selectors/conversations'; import { MemoConversationListItemWithDetails } from '../ConversationListItem'; +import styled from 'styled-components'; export enum SessionClosableOverlayType { Message = 'message', @@ -287,6 +288,12 @@ export class SessionClosableOverlay extends React.Component { } } +const MessageRequestListContainer = styled.div` + width: 100%; + overflow-y: auto; + border: var(--border-session); +`; + /** * A request needs to be be unapproved and not blocked to be valid. * @returns List of message request items @@ -294,7 +301,7 @@ export class SessionClosableOverlay extends React.Component { const MessageRequestList = () => { const conversationRequests = useSelector(getConversationRequests); return ( -
+ {conversationRequests.map(conversation => { return ( { /> ); })} -
+ ); }; diff --git a/ts/opengroup/utils/OpenGroupUtils.ts b/ts/opengroup/utils/OpenGroupUtils.ts index d72048daf..648f6f79f 100644 --- a/ts/opengroup/utils/OpenGroupUtils.ts +++ b/ts/opengroup/utils/OpenGroupUtils.ts @@ -40,7 +40,7 @@ export const openGroupPrefix = 'publicChat:'; export const openGroupPrefixRegex = new RegExp(`^${openGroupPrefix}`); export const openGroupV2ConversationIdRegex = new RegExp( - `${openGroupPrefix}${roomIdV2Regex}@${protocolRegex.source}${hostnameRegex.source}${portRegex}` + `${openGroupPrefix}${roomIdV2Regex}@${openGroupV2ServerUrlRegex.source}` ); /** diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 1de41ac0f..3775a17e4 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -1,9 +1,4 @@ -import { - getAllConversations, - getAllGroupsInvolvingId, - removeConversation, - saveConversation, -} from '../../data/data'; +import { getAllConversations, removeConversation, saveConversation } from '../../data/data'; import { ConversationAttributes, ConversationCollection, @@ -181,11 +176,6 @@ export class ConversationController { }); } - public async getAllGroupsInvolvingId(id: string) { - const groups = await getAllGroupsInvolvingId(id); - return groups.map((group: any) => this.conversations.add(group)); - } - public async deleteContact(id: string) { if (!this._initialFetchComplete) { throw new Error('getConversationController().get() needs complete initial fetch'); From 48e7a0e25f92548e6f180246291454dee3c9b951 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 8 Dec 2021 14:15:54 +1100 Subject: [PATCH 59/70] Various UI fixes (#2070) * cleanup unused convo json fields in db * display a toast if the user is not approved yet on call OFFER received * enable CBR for calls * do not update active_at on configMessage if !!active_at * remove mkdirp dependency * disable call button if focused convo is blocked * quote: do not include the full body in quote, but just the first 100 * click on the edit profile qr code padding * Allow longer input for opengroup join overlay Fixes #2068 * Fix overlay feature for start new session button * make ringing depend on redux CALL status * turn ON read-receipt by default --- Gruntfile.js | 8 - _locales/en/messages.json | 2 + app/logging.js | 9 +- app/profile_images.js | 3 +- app/sql.js | 37 ++- js/logging.js | 3 - js/modules/debuglogs.js | 53 ----- package.json | 2 - stylesheets/_modal.scss | 4 +- stylesheets/_session_left_pane.scss | 6 - .../conversation/ConversationHeader.tsx | 4 +- ts/components/dialog/EditProfileDialog.tsx | 17 +- ts/components/session/ActionsPanel.tsx | 40 +++- .../session/LeftPaneContactSection.tsx | 2 +- .../session/LeftPaneMessageSection.tsx | 12 +- .../session/LeftPaneSectionHeader.tsx | 70 +++--- .../session/LeftPaneSettingSection.tsx | 2 +- .../session/MessageRequestsBanner.tsx | 6 +- .../session/SessionClosableOverlay.tsx | 3 +- .../session/SessionToastContainer.tsx | 2 +- .../session/menu/ConversationHeaderMenu.tsx | 2 - ts/components/session/menu/Menu.tsx | 28 --- .../settings/section/CategoryPrivacy.tsx | 2 +- ts/data/data.ts | 4 + ts/models/conversation.ts | 223 +++++++++--------- ts/models/message.ts | 12 - ts/models/messageType.ts | 2 +- .../opengroupV2/OpenGroupManagerV2.ts | 2 + ts/receiver/configMessage.ts | 9 +- ts/session/utils/RingingManager.ts | 2 +- ts/session/utils/Toast.tsx | 13 +- ts/session/utils/calling/CallManager.ts | 82 ++++--- ts/state/ducks/call.tsx | 6 + ts/state/ducks/section.tsx | 1 - ts/state/ducks/userConfig.tsx | 2 +- ts/state/selectors/conversations.ts | 7 + ts/types/LocalizerKeys.ts | 4 +- ts/util/accountManager.ts | 7 +- yarn.lock | 7 - 39 files changed, 340 insertions(+), 360 deletions(-) delete mode 100644 js/modules/debuglogs.js diff --git a/Gruntfile.js b/Gruntfile.js index 3e93ae5d1..365942e8f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,6 +1,4 @@ const importOnce = require('node-sass-import-once'); -const rimraf = require('rimraf'); -const mkdirp = require('mkdirp'); const sass = require('node-sass'); /* eslint-disable more/no-then, no-console */ @@ -39,7 +37,6 @@ module.exports = grunt => { 'js/curve/curve25519_wrapper.js', 'node_modules/libsodium/dist/modules/libsodium.js', 'node_modules/libsodium-wrappers/dist/modules/libsodium-wrappers.js', - 'libtextsecure/libsignal-protocol.js', 'js/util_worker_tasks.js', ]; @@ -169,11 +166,6 @@ module.exports = grunt => { updateLocalConfig({ commitHash: hash }); }); - grunt.registerTask('clean-release', () => { - rimraf.sync('release'); - mkdirp.sync('release'); - }); - grunt.registerTask('dev', ['default', 'watch']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('default', [ diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 50b27172b..182541cc9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -156,6 +156,7 @@ "spellCheckDirty": "You must restart Session to apply your new settings", "notifications": "Notifications", "readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).", + "readReceiptDialogDescription": "Read Receipts are now turned ON by default. Click \"Cancel\" to turn them down.", "readReceiptSettingTitle": "Read Receipts", "typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).", "typingIndicatorsSettingTitle": "Typing Indicators", @@ -458,6 +459,7 @@ "noAudioOutputFound": "No audio output found", "callMediaPermissionsTitle": "Voice and video calls", "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.", + "callMissedNotApproved": "Call missed from '$name$' as you haven't approved this conversation yet. Send a message to him first.", "callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users", "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", "menuCall": "Call", diff --git a/app/logging.js b/app/logging.js index 95a87d67e..0e5a7c025 100644 --- a/app/logging.js +++ b/app/logging.js @@ -6,7 +6,6 @@ const fs = require('fs'); const electron = require('electron'); const bunyan = require('bunyan'); -const mkdirp = require('mkdirp'); const _ = require('lodash'); const readFirstLine = require('firstline'); const readLastLines = require('read-last-lines').read; @@ -31,7 +30,7 @@ function initialize() { const basePath = app.getPath('userData'); const logPath = path.join(basePath, 'logs'); - mkdirp.sync(logPath); + fs.mkdirSync(logPath, { recursive: true }); return cleanupLogs(logPath).then(() => { if (logger) { @@ -63,7 +62,7 @@ function initialize() { }); ipc.on('fetch-log', event => { - mkdirp.sync(logPath); + fs.mkdirSync(logPath, { recursive: true }); fetch(logPath).then( data => { @@ -125,7 +124,7 @@ async function cleanupLogs(logPath) { // delete and re-create the log directory await deleteAllLogs(logPath); - mkdirp.sync(logPath); + fs.mkdirSync(logPath, { recursive: true }); } } @@ -221,7 +220,7 @@ function fetch(logPath) { // Check that the file exists locally if (!fs.existsSync(logPath)) { console._log('Log folder not found while fetching its content. Quick! Creating it.'); - mkdirp.sync(logPath); + fs.mkdirSync(logPath, { recursive: true }); } const files = fs.readdirSync(logPath); const paths = files.map(file => path.join(logPath, file)); diff --git a/app/profile_images.js b/app/profile_images.js index 75b16a0c8..b2752215c 100644 --- a/app/profile_images.js +++ b/app/profile_images.js @@ -1,12 +1,11 @@ const fs = require('fs'); -const mkdirp = require('mkdirp'); const path = require('path'); const { app } = require('electron').remote; const userDataPath = app.getPath('userData'); const PATH = path.join(userDataPath, 'profileImages'); -mkdirp.sync(PATH); +fs.mkdirSync(PATH, { recursive: true }); const hasImage = pubKey => fs.existsSync(getImagePath(pubKey)); diff --git a/app/sql.js b/app/sql.js index 0948d915b..d4b20e0db 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1,5 +1,5 @@ const path = require('path'); -const mkdirp = require('mkdirp'); +const fs = require('fs'); const rimraf = require('rimraf'); const SQL = require('better-sqlite3'); const { app, dialog, clipboard } = require('electron'); @@ -72,6 +72,7 @@ module.exports = { getNextExpiringMessage, getMessagesByConversation, getFirstUnreadMessageIdInConversation, + hasConversationOutgoingMessage, getUnprocessedCount, getAllUnprocessed, @@ -1240,7 +1241,11 @@ function updateToLokiSchemaVersion17(currentVersion, db) { UPDATE ${CONVERSATIONS_TABLE} SET json = json_set(json, '$.isApproved', 1) `); - + // remove the moderators field. As it was only used for opengroups a long time ago and whatever is there is probably unused + db.exec(` + UPDATE ${CONVERSATIONS_TABLE} SET + json = json_remove(json, '$.moderators', '$.dataMessage', '$.accessKey', '$.profileSharing', '$.sessionRestoreSeen') + `); writeLokiSchemaVersion(targetVersion, db); })(); console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); @@ -1311,8 +1316,7 @@ let databaseFilePath; function _initializePaths(configDir) { const dbDir = path.join(configDir, 'sql'); - mkdirp.sync(dbDir); - + fs.mkdirSync(dbDir, { recursive: true }); databaseFilePath = path.join(dbDir, 'db.sqlite'); } @@ -1357,7 +1361,9 @@ function initialize({ configDir, key, messages, passwordAttempt }) { // Clear any already deleted db entries on each app start. vacuumDatabase(db); const msgCount = getMessageCount(); - console.warn('total message count: ', msgCount); + const convoCount = getConversationCount(); + console.info('total message count: ', msgCount); + console.info('total conversation count: ', convoCount); } catch (error) { if (passwordAttempt) { throw error; @@ -2171,6 +2177,27 @@ function getMessagesByConversation( return map(rows, row => jsonToObject(row.json)); } +function hasConversationOutgoingMessage(conversationId) { + const row = globalInstance + .prepare( + ` + SELECT count(*) FROM ${MESSAGES_TABLE} WHERE + conversationId = $conversationId AND + type IS 'outgoing' + ` + ) + .get({ + conversationId, + }); + if (!row) { + throw new Error('hasConversationOutgoingMessage: Unable to get coun'); + } + + console.warn('hasConversationOutgoingMessage', row); + + return Boolean(row['count(*)']); +} + function getFirstUnreadMessageIdInConversation(conversationId) { const rows = globalInstance .prepare( diff --git a/js/logging.js b/js/logging.js index 96b1f632e..bb5a8020d 100644 --- a/js/logging.js +++ b/js/logging.js @@ -6,7 +6,6 @@ const { ipcRenderer } = require('electron'); const _ = require('lodash'); -const debuglogs = require('./modules/debuglogs'); const Privacy = require('./modules/privacy'); const ipc = ipcRenderer; @@ -100,7 +99,6 @@ function fetch() { }); } -const publish = debuglogs.upload; const development = window.getEnvironment() !== 'production'; // A modern logging interface for the browser @@ -127,7 +125,6 @@ window.log = { debug: _.partial(logAtLevel, 'debug', 'DEBUG'), trace: _.partial(logAtLevel, 'trace', 'TRACE'), fetch, - publish, }; window.onerror = (message, script, line, col, error) => { diff --git a/js/modules/debuglogs.js b/js/modules/debuglogs.js deleted file mode 100644 index 3fd76dd29..000000000 --- a/js/modules/debuglogs.js +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-env node */ -/* global window */ - -const FormData = require('form-data'); -const insecureNodeFetch = require('node-fetch'); - -const BASE_URL = 'https://debuglogs.org'; -const VERSION = window.getVersion(); -const USER_AGENT = `Session ${VERSION}`; - -// upload :: String -> Promise URL -exports.upload = async content => { - window.log.warn('insecureNodeFetch => upload debugLogs'); - const signedForm = await insecureNodeFetch(BASE_URL, { - headers: { - 'user-agent': USER_AGENT, - }, - }); - const json = await signedForm.json(); - if (!signedForm.ok || !json) { - throw new Error('Failed to retrieve token'); - } - const { fields, url } = json; - - const form = new FormData(); - // The API expects `key` to be the first field: - form.append('key', fields.key); - Object.entries(fields) - .filter(([key]) => key !== 'key') - .forEach(([key, value]) => { - form.append(key, value); - }); - - const contentBuffer = Buffer.from(content, 'utf8'); - const contentType = 'text/plain'; - form.append('Content-Type', contentType); - form.append('file', contentBuffer, { - contentType, - filename: `session-desktop-debug-log-${VERSION}.txt`, - }); - - const result = await insecureNodeFetch(url, { - method: 'POST', - body: form, - }); - - const { status } = result; - if (status !== 204) { - throw new Error(`Failed to upload to S3, got status ${status}`); - } - - return `${BASE_URL}/${fields.key}`; -}; diff --git a/package.json b/package.json index 5ae1ae014..1dceee41f 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ "lodash": "4.17.11", "long": "^4.0.0", "mic-recorder-to-mp3": "^2.2.2", - "mkdirp": "0.5.1", "moment": "2.21.0", "mustache": "2.3.0", "nan": "2.14.2", @@ -153,7 +152,6 @@ "@types/libsodium-wrappers": "^0.7.8", "@types/linkify-it": "2.0.3", "@types/lodash": "4.14.106", - "@types/mkdirp": "0.5.2", "@types/mocha": "5.0.0", "@types/node-fetch": "^2.5.7", "@types/pify": "3.0.2", diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index 6d15be954..2387092a6 100644 --- a/stylesheets/_modal.scss +++ b/stylesheets/_modal.scss @@ -226,8 +226,8 @@ justify-content: center; position: absolute; right: -3px; - height: 26px; - width: 26px; + height: 30px; + width: 30px; border-radius: 50%; background-color: $session-color-white; transition: $session-transition-duration; diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index ade13d4d4..a547920a9 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -149,12 +149,6 @@ $session-compose-margin: 20px; &__list { height: -webkit-fill-available; - - &-popup { - width: -webkit-fill-available; - height: -webkit-fill-available; - position: absolute; - } } &-overlay { diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 43b3270fe..9d748a52c 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -13,6 +13,7 @@ import { getConversationHeaderProps, getConversationHeaderTitleProps, getCurrentNotificationSettingText, + getIsSelectedBlocked, getIsSelectedNoteToSelf, getIsSelectedPrivate, getSelectedConversationIsPublic, @@ -198,6 +199,7 @@ const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => const CallButton = () => { const isPrivate = useSelector(getIsSelectedPrivate); + const isBlocked = useSelector(getIsSelectedBlocked); const isMe = useSelector(getIsSelectedNoteToSelf); const selectedConvoKey = useSelector(getSelectedConversationKey); @@ -205,7 +207,7 @@ const CallButton = () => { const hasOngoingCall = useSelector(getHasOngoingCall); const canCall = !(hasIncomingCall || hasOngoingCall); - if (!isPrivate || isMe || !selectedConvoKey) { + if (!isPrivate || isMe || !selectedConvoKey || isBlocked) { return null; } diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index 045e506cb..31038fb32 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -150,15 +150,14 @@ export class EditProfileDialog extends React.Component<{}, State> { name="name" onChange={this.onFileSelected} /> -
- { - this.setState(state => ({ ...state, mode: 'qr' })); - }} - /> +
{ + this.setState(state => ({ ...state, mode: 'qr' })); + }} + role="button" + > +
diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 7ad2f323b..ef7800b1a 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { Dispatch, useEffect, useState } from 'react'; import { SessionIconButton } from './icon'; import { Avatar, AvatarSize } from '../Avatar'; import { SessionToastContainer } from './SessionToastContainer'; @@ -6,6 +6,7 @@ import { getConversationController } from '../../session/conversations'; import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; import { + createOrUpdateItem, generateAttachmentKeyIfEmpty, getAllOpenGroupV1Conversations, getItemById, @@ -36,7 +37,11 @@ import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; import { getSwarmPollingInstance } from '../../session/snode_api'; import { DURATION } from '../../session/constants'; import { conversationChanged, conversationRemoved } from '../../state/ducks/conversations'; -import { editProfileModal, onionPathModal } from '../../state/ducks/modalDialog'; +import { + editProfileModal, + onionPathModal, + updateConfirmModal, +} from '../../state/ducks/modalDialog'; import { uploadOurAvatar } from '../../interactions/conversationInteractions'; import { ModalContainer } from '../dialog/ModalContainer'; import { debounce } from 'underscore'; @@ -49,7 +54,29 @@ import { switchHtmlToDarkTheme, switchHtmlToLightTheme } from '../../state/ducks import { DraggableCallContainer } from './calling/DraggableCallContainer'; import { IncomingCallDialog } from './calling/IncomingCallDialog'; import { CallInFullScreenContainer } from './calling/CallInFullScreenContainer'; - +import { SessionButtonColor } from './SessionButton'; +import { settingsReadReceipt } from './settings/section/CategoryPrivacy'; + +async function showTurnOnReadAck(dispatch: Dispatch) { + const singleShotSettingId = 'read-receipt-turn-on-asked'; + const item = (await getItemById(singleShotSettingId))?.value || false; + + if (!item) { + await createOrUpdateItem({ id: singleShotSettingId, value: true }); + // set it to true by default, user will be asked to willingfully turn it off + window.setSettingValue(settingsReadReceipt, true); + dispatch( + updateConfirmModal({ + title: window.i18n('readReceiptSettingTitle'), + messageSub: window.i18n('readReceiptDialogDescription'), + okTheme: SessionButtonColor.Green, + onClickCancel: () => { + window.setSettingValue(settingsReadReceipt, false); + }, + }) + ); + } +} const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); const unreadMessageCount = useSelector(getUnreadMessageCount); @@ -227,7 +254,7 @@ const triggerAvatarReUploadIfNeeded = async () => { /** * This function is called only once: on app startup with a logged in user */ -const doAppStartUp = () => { +const doAppStartUp = (dispatch: Dispatch) => { // init the messageQueue. In the constructor, we add all not send messages // this call does nothing except calling the constructor, which will continue sending message in the pipeline void getMessageQueue().processAllPending(); @@ -246,6 +273,8 @@ const doAppStartUp = () => { void loadDefaultRooms(); + void showTurnOnReadAck(dispatch); + debounce(triggerAvatarReUploadIfNeeded, 200); }; @@ -267,10 +296,11 @@ export const ActionsPanel = () => { const [startCleanUpMedia, setStartCleanUpMedia] = useState(false); const ourPrimaryConversation = useSelector(getOurPrimaryConversation); + const dispatch = useDispatch(); // this maxi useEffect is called only once: when the component is mounted. // For the action panel, it means this is called only one per app start/with a user loggedin useEffect(() => { - void doAppStartUp(); + void doAppStartUp(dispatch); }, []); // wait for cleanUpMediasInterval and then start cleaning up medias diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index e1f2693dc..e1f8a931c 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -53,7 +53,7 @@ const ContactListItemSection = () => { export const LeftPaneContactSection = () => { return (
- +
diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 5407b0490..4d0342b56 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -139,22 +139,12 @@ export class LeftPaneMessageSection extends React.Component { } } - public renderHeader(): JSX.Element { - return ( - - ); - } - public render(): JSX.Element { const { overlay } = this.state; return (
- {this.renderHeader()} + {overlay ? this.renderClosableOverlay() : this.renderConversations()}
); diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx index 3e734e827..07b5a8df8 100644 --- a/ts/components/session/LeftPaneSectionHeader.tsx +++ b/ts/components/session/LeftPaneSectionHeader.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import classNames from 'classnames'; -import { SessionIcon, SessionIconType } from './icon'; import styled from 'styled-components'; import { SessionButton, SessionButtonType } from './SessionButton'; import { useDispatch, useSelector } from 'react-redux'; @@ -11,52 +9,42 @@ import { Flex } from '../basic/Flex'; import { getFocusedSection } from '../../state/selectors/section'; import { SectionType } from '../../state/ducks/section'; import { UserUtils } from '../../session/utils'; +import { SessionIcon } from './icon'; -const Tab = ({ - isSelected, - label, - onSelect, - type, -}: { - isSelected: boolean; - label: string; - onSelect?: (event: number) => void; - type: number; -}) => { - const handleClick = onSelect - ? () => { - onSelect(type); - } - : undefined; - - return ( -

- {label} -

- ); -}; - -type Props = { - label?: string; - buttonIcon?: SessionIconType; - buttonClicked?: any; -}; +const SectionTitle = styled.h1` + padding: 0 var(--margins-sm); + flex-grow: 1; + color: var(--color-text); +`; -export const LeftPaneSectionHeader = (props: Props) => { - const { label, buttonIcon, buttonClicked } = props; +export const LeftPaneSectionHeader = (props: { buttonClicked?: any }) => { const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt); + const focusedSection = useSelector(getFocusedSection); + + let label: string | undefined; + + const isMessageSection = focusedSection === SectionType.Message; + + switch (focusedSection) { + case SectionType.Contact: + label = window.i18n('contactsHeader'); + break; + case SectionType.Settings: + label = window.i18n('settingsHeader'); + break; + case SectionType.Message: + label = window.i18n('messagesHeader'); + break; + default: + } return (
- {label && } - {buttonIcon && ( - - + {label} + {isMessageSection && ( + + )}
diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx index 73ba038bf..1f5ff2181 100644 --- a/ts/components/session/LeftPaneSettingSection.tsx +++ b/ts/components/session/LeftPaneSettingSection.tsx @@ -113,7 +113,7 @@ const LeftPaneBottomButtons = () => { export const LeftPaneSettingSection = () => { return (
- +
diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx index e19642439..3869da384 100644 --- a/ts/components/session/MessageRequestsBanner.tsx +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -26,9 +26,9 @@ const StyledMessageRequestBannerHeader = styled.span` font-weight: bold; font-size: 15px; color: var(--color-text-subtle); - padding-left: var(--margin-xs); + padding-left: var(--margins-xs); margin-inline-start: 12px; - margin-top: var(--margin-sm); + margin-top: var(--margins-sm); line-height: 18px; overflow-x: hidden; overflow-y: hidden; @@ -37,7 +37,7 @@ const StyledMessageRequestBannerHeader = styled.span` `; const StyledCircleIcon = styled.div` - padding-left: var(--margin-xs); + padding-left: var(--margins-xs); `; const StyledUnreadCounter = styled.div` diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 897be4bb7..a7ee1b402 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -13,6 +13,7 @@ import { useSelector } from 'react-redux'; import { getConversationRequests } from '../../state/selectors/conversations'; import { MemoConversationListItemWithDetails } from '../ConversationListItem'; import styled from 'styled-components'; +// tslint:disable: use-simple-attributes export enum SessionClosableOverlayType { Message = 'message', @@ -178,7 +179,7 @@ export class SessionClosableOverlay extends React.Component { placeholder={placeholder} value={groupName} isGroup={true} - maxLength={100} + maxLength={isOpenGroupView ? 300 : 100} onChange={this.onGroupNameChanged} onPressEnter={() => onButtonClick(groupName, selectedMembers)} /> diff --git a/ts/components/session/SessionToastContainer.tsx b/ts/components/session/SessionToastContainer.tsx index 7788a1e0b..8beeb647d 100644 --- a/ts/components/session/SessionToastContainer.tsx +++ b/ts/components/session/SessionToastContainer.tsx @@ -6,7 +6,7 @@ const SessionToastContainerPrivate = () => { return ( { return ( - {getStartCallMenuItem(conversationId)} {getDisappearingMenuItem(isPublic, isKickedFromGroup, left, isBlocked, conversationId)} {getNotificationForConvoMenuItem({ isKickedFromGroup, diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 9b05d96d5..08ac8643c 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call'; import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; import { getFocusedSection } from '../../../state/selectors/section'; import { Item, Submenu } from 'react-contexify'; @@ -18,7 +17,6 @@ import { SectionType } from '../../../state/ducks/section'; import { getConversationController } from '../../../session/conversations'; import { blockConvoById, - callRecipient, clearNickNameByConvoId, copyPublicKeyByConvoId, deleteAllMessagesByConvoIdWithConfirmation, @@ -347,32 +345,6 @@ export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | nu ); } -export function getStartCallMenuItem(conversationId: string): JSX.Element | null { - if (window?.lokiFeatureFlags.useCallMessage) { - const convoOut = getConversationController().get(conversationId); - // we don't support calling groups - - const hasIncomingCall = useSelector(getHasIncomingCall); - const hasOngoingCall = useSelector(getHasOngoingCall); - const canCall = !(hasIncomingCall || hasOngoingCall); - if (!convoOut?.isPrivate() || convoOut.isMe()) { - return null; - } - - return ( - { - void callRecipient(conversationId, canCall); - }} - > - {window.i18n('menuCall')} - - ); - } - - return null; -} - export function getDisappearingMenuItem( isPublic: boolean | undefined, isKickedFromGroup: boolean | undefined, diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx index 0ed432602..c6eea2df3 100644 --- a/ts/components/session/settings/section/CategoryPrivacy.tsx +++ b/ts/components/session/settings/section/CategoryPrivacy.tsx @@ -10,7 +10,7 @@ import { PasswordAction } from '../../../dialog/SessionPasswordDialog'; import { SessionButtonColor } from '../../SessionButton'; import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem'; -const settingsReadReceipt = 'read-receipt-setting'; +export const settingsReadReceipt = 'read-receipt-setting'; const settingsTypingIndicator = 'typing-indicators-setting'; const settingsAutoUpdate = 'auto-update'; diff --git a/ts/data/data.ts b/ts/data/data.ts index 6924c6355..e5cc48e59 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -120,6 +120,7 @@ const channelsToMake = { getNextExpiringMessage, getMessagesByConversation, getFirstUnreadMessageIdInConversation, + hasConversationOutgoingMessage, getSeenMessagesByHashList, getLastHashBySnode, @@ -763,6 +764,9 @@ export async function getFirstUnreadMessageIdInConversation( return channels.getFirstUnreadMessageIdInConversation(conversationId); } +export async function hasConversationOutgoingMessage(conversationId: string): Promise { + return channels.hasConversationOutgoingMessage(conversationId); +} export async function getLastHashBySnode(convoId: string, snode: string): Promise { return channels.getLastHashBySnode(convoId, snode); } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index b2209829f..12bec5b3a 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -82,12 +82,10 @@ export interface ConversationAttributes { active_at: number; lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group groupAdmins?: Array; - moderators?: Array; // TODO to merge to groupAdmins with a migration on the db isKickedFromGroup?: boolean; avatarPath?: string; isMe?: boolean; subscriberCount?: number; - sessionRestoreSeen?: boolean; is_medium_group?: boolean; type: string; avatarPointer?: string; @@ -124,12 +122,10 @@ export interface ConversationAttributesOptionals { timestamp?: number; // timestamp of what? lastJoinedTimestamp?: number; groupAdmins?: Array; - moderators?: Array; isKickedFromGroup?: boolean; avatarPath?: string; isMe?: boolean; subscriberCount?: number; - sessionRestoreSeen?: boolean; is_medium_group?: boolean; type: string; avatarPointer?: string; @@ -164,11 +160,9 @@ export const fillConvoAttributesWithDefaults = ( lastMessageStatus: null, lastJoinedTimestamp: new Date('1970-01-01Z00:00:00:000').getTime(), groupAdmins: [], - moderators: [], isKickedFromGroup: false, isMe: false, subscriberCount: 0, - sessionRestoreSeen: false, is_medium_group: false, lastMessage: null, expireTimer: 0, @@ -280,6 +274,7 @@ export class ConversationModel extends Backbone.Model { public isMediumGroup() { return this.get('is_medium_group'); } + /** * Returns true if this conversation is active * i.e. the conversation is visibie on the left pane. (Either we or another user created this convo). @@ -290,99 +285,6 @@ export class ConversationModel extends Backbone.Model { return Boolean(this.get('active_at')); } - public async bumpTyping() { - // We don't send typing messages if the setting is disabled - // or we blocked that user - if ( - this.isPublic() || - this.isMediumGroup() || - !this.isActive() || - !window.storage.get('typing-indicators-setting') || - this.isBlocked() - ) { - return; - } - - if (!this.typingRefreshTimer) { - const isTyping = true; - this.setTypingRefreshTimer(); - this.sendTypingMessage(isTyping); - } - - this.setTypingPauseTimer(); - } - - public setTypingRefreshTimer() { - if (this.typingRefreshTimer) { - global.clearTimeout(this.typingRefreshTimer); - } - this.typingRefreshTimer = global.setTimeout(this.onTypingRefreshTimeout.bind(this), 10 * 1000); - } - - public onTypingRefreshTimeout() { - const isTyping = true; - this.sendTypingMessage(isTyping); - - // This timer will continue to reset itself until the pause timer stops it - this.setTypingRefreshTimer(); - } - - public setTypingPauseTimer() { - if (this.typingPauseTimer) { - global.clearTimeout(this.typingPauseTimer); - } - this.typingPauseTimer = global.setTimeout(this.onTypingPauseTimeout.bind(this), 10 * 1000); - } - - public onTypingPauseTimeout() { - const isTyping = false; - this.sendTypingMessage(isTyping); - - this.clearTypingTimers(); - } - - public clearTypingTimers() { - if (this.typingPauseTimer) { - global.clearTimeout(this.typingPauseTimer); - this.typingPauseTimer = null; - } - if (this.typingRefreshTimer) { - global.clearTimeout(this.typingRefreshTimer); - this.typingRefreshTimer = null; - } - } - - public sendTypingMessage(isTyping: boolean) { - if (!this.isPrivate()) { - return; - } - - const recipientId = this.id; - - if (!recipientId) { - throw new Error('Need to provide either recipientId'); - } - - const primaryDevicePubkey = window.storage.get('primaryDevicePubKey'); - if (recipientId && primaryDevicePubkey === recipientId) { - // note to self - return; - } - - const typingParams = { - timestamp: Date.now(), - isTyping, - typingTimestamp: Date.now(), - }; - const typingMessage = new TypingMessage(typingParams); - - // send the message to a single recipient if this is a session chat - const device = new PubKey(recipientId); - getMessageQueue() - .sendToPubKey(device, typingMessage) - .catch(window?.log?.error); - } - public async cleanup() { const { deleteAttachmentData } = window.Signal.Migrations; await window.Signal.Types.Conversation.deleteExternalFiles(this.attributes, { @@ -409,12 +311,10 @@ export class ConversationModel extends Backbone.Model { // removeMessage(); } - public getGroupAdmins() { + public getGroupAdmins(): Array { const groupAdmins = this.get('groupAdmins'); - if (groupAdmins?.length) { - return groupAdmins; - } - return this.get('moderators'); + + return groupAdmins && groupAdmins?.length > 0 ? groupAdmins : []; } // tslint:disable-next-line: cyclomatic-complexity @@ -558,9 +458,6 @@ export class ConversationModel extends Backbone.Model { const newAdmins = _.uniq(_.sortBy(groupAdmins)); if (_.isEqual(existingAdmins, newAdmins)) { - // window?.log?.info( - // 'Skipping updates of groupAdmins/moderators. No change detected.' - // ); return; } this.set({ groupAdmins }); @@ -694,7 +591,8 @@ export class ConversationModel extends Backbone.Model { return { author: quotedMessage.getSource(), id: `${quotedMessage.get('sent_at')}` || '', - text: body, + // no need to quote the full message length. + text: body?.slice(0, 100), attachments: quotedAttachments, timestamp: quotedMessage.get('sent_at') || 0, convoId: this.id, @@ -1626,7 +1524,6 @@ export class ConversationModel extends Backbone.Model { await this.commit(); } } else { - // tslint:disable-next-line: no-dynamic-delete this.typingTimer = null; if (wasTyping) { // User was previously typing, and is no longer. State change! @@ -1635,7 +1532,7 @@ export class ConversationModel extends Backbone.Model { } } - public async clearContactTypingTimer(_sender: string) { + private async clearContactTypingTimer(_sender: string) { if (!!this.typingTimer) { global.clearTimeout(this.typingTimer); this.typingTimer = null; @@ -1654,6 +1551,112 @@ export class ConversationModel extends Backbone.Model { return typeof expireTimer === 'number' && expireTimer > 0; } + + private shouldDoTyping() { + // for typing to happen, this must be a private unblocked active convo, and the settings to be on + if ( + !this.isActive() || + !window.storage.get('typing-indicators-setting') || + this.isBlocked() || + !this.isPrivate() + ) { + return false; + } + const msgRequestsEnabled = + window.lokiFeatureFlags.useMessageRequests && + window.inboxStore?.getState().userConfig.messageRequests; + + // if msg requests are unused, we have to send typing (this is already a private active unblocked convo) + if (!msgRequestsEnabled) { + return true; + } + // with message requests in use, we just need to check for isApproved + return Boolean(this.get('isApproved')); + } + + private async bumpTyping() { + if (!this.shouldDoTyping()) { + return; + } + + if (!this.typingRefreshTimer) { + const isTyping = true; + this.setTypingRefreshTimer(); + this.sendTypingMessage(isTyping); + } + + this.setTypingPauseTimer(); + } + + private setTypingRefreshTimer() { + if (this.typingRefreshTimer) { + global.clearTimeout(this.typingRefreshTimer); + } + this.typingRefreshTimer = global.setTimeout(this.onTypingRefreshTimeout.bind(this), 10 * 1000); + } + + private onTypingRefreshTimeout() { + const isTyping = true; + this.sendTypingMessage(isTyping); + + // This timer will continue to reset itself until the pause timer stops it + this.setTypingRefreshTimer(); + } + + private setTypingPauseTimer() { + if (this.typingPauseTimer) { + global.clearTimeout(this.typingPauseTimer); + } + this.typingPauseTimer = global.setTimeout(this.onTypingPauseTimeout.bind(this), 10 * 1000); + } + + private onTypingPauseTimeout() { + const isTyping = false; + this.sendTypingMessage(isTyping); + + this.clearTypingTimers(); + } + + private clearTypingTimers() { + if (this.typingPauseTimer) { + global.clearTimeout(this.typingPauseTimer); + this.typingPauseTimer = null; + } + if (this.typingRefreshTimer) { + global.clearTimeout(this.typingRefreshTimer); + this.typingRefreshTimer = null; + } + } + + private sendTypingMessage(isTyping: boolean) { + if (!this.isPrivate()) { + return; + } + + const recipientId = this.id; + + if (!recipientId) { + throw new Error('Need to provide either recipientId'); + } + + if (this.isMe()) { + // note to self + return; + } + + const typingParams = { + timestamp: Date.now(), + isTyping, + typingTimestamp: Date.now(), + }; + const typingMessage = new TypingMessage(typingParams); + + // send the message to a single recipient if this is a session chat + const device = new PubKey(recipientId); + getMessageQueue() + .sendToPubKey(device, typingMessage) + .catch(window?.log?.error); + } } export class ConversationCollection extends Backbone.Collection { diff --git a/ts/models/message.ts b/ts/models/message.ts index f7f75bdee..d41e9a5c4 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1117,18 +1117,6 @@ export class MessageModel extends Backbone.Model { await this.commit(); } - public async markMessageSyncOnly(dataMessage: DataMessage) { - this.set({ - // These are the same as a normal send() - dataMessage, - sent_to: [UserUtils.getOurPubKeyStrFromCache()], - sent: true, - expirationStartTimestamp: Date.now(), - }); - - await this.commit(); - } - public async saveErrors(providedErrors: any) { let errors = providedErrors; diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 643b511d2..0730f51ea 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -49,7 +49,7 @@ export interface MessageAttributes { */ timestamp?: number; status?: MessageDeliveryStatus; - dataMessage: any; + // dataMessage: any; sent_to: any; sent: boolean; diff --git a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts index 5c7fb0738..cfa4278f6 100644 --- a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts @@ -199,10 +199,12 @@ export class OpenGroupManagerV2 { await saveV2OpenGroupRoom(room); // mark active so it's not in the contacts list but in the conversation list + // mark isApproved as this is a public chat conversation.set({ active_at: Date.now(), name: room.roomName, avatarPath: room.roomName, + isApproved: true, }); await conversation.commit(); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index e396d76a1..ae2d563df 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -123,7 +123,7 @@ const handleContactReceived = async ( envelope: EnvelopePlus ) => { try { - if (!contactReceived.publicKey) { + if (!contactReceived.publicKey?.length) { return; } const contactConvo = await getConversationController().getOrCreateAndWait( @@ -134,8 +134,11 @@ const handleContactReceived = async ( displayName: contactReceived.name, profilePictre: contactReceived.profilePicture, }; - // updateProfile will do a commit for us - contactConvo.set('active_at', _.toNumber(envelope.timestamp)); + + const existingActiveAt = contactConvo.get('active_at'); + if (!existingActiveAt || existingActiveAt === 0) { + contactConvo.set('active_at', _.toNumber(envelope.timestamp)); + } if ( window.lokiFeatureFlags.useMessageRequests && diff --git a/ts/session/utils/RingingManager.ts b/ts/session/utils/RingingManager.ts index 5357afb3d..9bd62b6a8 100644 --- a/ts/session/utils/RingingManager.ts +++ b/ts/session/utils/RingingManager.ts @@ -17,7 +17,7 @@ function startRinging() { ringingAudio.loop = true; ringingAudio.volume = 0.6; } - void ringingAudio.play(); + void ringingAudio.play().catch(window.log.info); } export function getIsRinging() { diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 36541e582..722005e12 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -24,7 +24,8 @@ export function pushToastInfo( id: string, title: string, description?: string, - onToastClick?: () => void + onToastClick?: () => void, + delay?: number ) { toast.info( , - { toastId: id, updateId: id } + { toastId: id, updateId: id, delay } ); } @@ -166,6 +167,14 @@ export function pushedMissedCallCauseOfPermission(conversationName: string) { ); } +export function pushedMissedCallNotApproved(displayName: string) { + pushToastInfo( + 'missedCall', + window.i18n('callMissedTitle'), + window.i18n('callMissedNotApproved', [displayName]) + ); +} + export function pushVideoCallPermissionNeeded() { pushToastInfo( 'videoCallPermissionNeeded', diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index b318a0bf0..80b1a5430 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -20,11 +20,12 @@ import { PubKey } from '../../types'; import { v4 as uuidv4 } from 'uuid'; import { PnServer } from '../../../pushnotification'; -import { getIsRinging, setIsRinging } from '../RingingManager'; +import { getIsRinging } from '../RingingManager'; import { getBlackSilenceMediaStream } from './Silence'; import { getMessageQueue } from '../..'; import { MessageSender } from '../../sending'; import { DURATION } from '../../constants'; +import { hasConversationOutgoingMessage } from '../../../data/data'; // tslint:disable: function-name @@ -386,10 +387,20 @@ async function createOfferAndSendIt(recipient: string) { } if (offer && offer.sdp) { + const lines = offer.sdp.split(/\r?\n/); + const lineWithFtmpIndex = lines.findIndex(f => f.startsWith('a=fmtp:111')); + const partBeforeComma = lines[lineWithFtmpIndex].split(';'); + lines[lineWithFtmpIndex] = `${partBeforeComma[0]};cbr=1`; + let overridenSdps = lines.join('\n'); + overridenSdps = overridenSdps.replace( + new RegExp('.+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\\r?\\n'), + '' + ); + const offerMessage = new CallMessage({ timestamp: Date.now(), type: SignalService.CallMessage.Type.OFFER, - sdps: [offer.sdp], + sdps: [overridenSdps], uuid: currentCallUUID, }); @@ -497,7 +508,6 @@ export async function USER_callRecipient(recipient: string) { void PnServer.notifyPnServer(wrappedEnvelope, recipient); await openMediaDevicesAndAddTracks(); - setIsRinging(true); await createOfferAndSendIt(recipient); // close and end the call if callTimeoutMs is reached ans still not connected @@ -583,7 +593,6 @@ function handleConnectionStateChanged(pubkey: string) { if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { - setIsRinging(false); const firstAudioInput = audioInputsList?.[0].deviceId || undefined; if (firstAudioInput) { void selectAudioInputByDeviceId(firstAudioInput); @@ -603,7 +612,6 @@ function handleConnectionStateChanged(pubkey: string) { function closeVideoCall() { window.log.info('closingVideoCall '); currentCallStartTimestamp = undefined; - setIsRinging(false); if (peerConnection) { peerConnection.ontrack = null; peerConnection.onicecandidate = null; @@ -687,7 +695,6 @@ function onDataChannelReceivedMessage(ev: MessageEvent) { } function onDataChannelOnOpen() { window.log.info('onDataChannelOnOpen: sending video status'); - setIsRinging(false); sendVideoStatusViaDataChannel(); } @@ -747,7 +754,6 @@ function createOrGetPeerConnection(withPubkey: string) { export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); - setIsRinging(false); if (currentCallUUID) { window.log.warn( 'Looks like we are already in a call as in USER_acceptIncomingCallRequest is not undefined' @@ -828,7 +834,6 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUID: string) { - setIsRinging(false); window.log.info(`rejectCallAlreadyAnotherCall ${ed25519Str(fromSender)}: ${forcedUUID}`); rejectedCallUUIDS.add(forcedUUID); const rejectCallMessage = new CallMessage({ @@ -843,7 +848,6 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI } export async function USER_rejectIncomingCallRequest(fromSender: string) { - setIsRinging(false); // close the popup call window.inboxStore?.dispatch(endCall()); const lastOfferMessage = findLastMessageTypeFromSender( @@ -943,8 +947,6 @@ export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: stri (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') ) { // remote user hangup an offer he sent but we did not accept it yet - setIsRinging(false); - window.inboxStore?.dispatch(endCall()); } } @@ -993,6 +995,18 @@ function getCachedMessageFromCallMessage( }; } +async function isUserApprovedOrWeSentAMessage(user: string) { + const isApproved = getConversationController() + .get(user) + ?.isApproved(); + + if (isApproved) { + return true; + } + + return hasConversationOutgoingMessage(user); +} + export async function handleCallTypeOffer( sender: string, callMessage: SignalService.CallMessage, @@ -1009,7 +1023,16 @@ export async function handleCallTypeOffer( const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg); - await handleMissedCall(sender, incomingOfferTimestamp, true); + await handleMissedCall(sender, incomingOfferTimestamp, 'permissions'); + return; + } + + const shouldDisplayOffer = await isUserApprovedOrWeSentAMessage(sender); + if (!shouldDisplayOffer) { + const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); + pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg); + + await handleMissedCall(sender, incomingOfferTimestamp, 'not-approved'); return; } @@ -1022,7 +1045,7 @@ export async function handleCallTypeOffer( return; } // add a message in the convo with this user about the missed call. - await handleMissedCall(sender, incomingOfferTimestamp, false); + await handleMissedCall(sender, incomingOfferTimestamp, 'another-call-ongoing'); // Here, we are in a call, and we got an offer from someone we are in a call with, and not one of his other devices. // Just hangup automatically the call on the calling side. @@ -1066,7 +1089,6 @@ export async function handleCallTypeOffer( } else if (callerConvo) { await callerConvo.notifyIncomingCall(); } - setIsRinging(true); } const cachedMessage = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp); @@ -1079,22 +1101,26 @@ export async function handleCallTypeOffer( export async function handleMissedCall( sender: string, incomingOfferTimestamp: number, - isBecauseOfCallPermission: boolean + reason: 'not-approved' | 'permissions' | 'another-call-ongoing' ) { const incomingCallConversation = getConversationController().get(sender); - setIsRinging(false); - if (!isBecauseOfCallPermission) { - ToastUtils.pushedMissedCall( - incomingCallConversation?.getNickname() || - incomingCallConversation?.getProfileName() || - 'Unknown' - ); - } else { - ToastUtils.pushedMissedCallCauseOfPermission( - incomingCallConversation?.getNickname() || - incomingCallConversation?.getProfileName() || - 'Unknown' - ); + + const displayname = + incomingCallConversation?.getNickname() || + incomingCallConversation?.getProfileName() || + 'Unknown'; + + switch (reason) { + case 'permissions': + ToastUtils.pushedMissedCallCauseOfPermission(displayname); + break; + case 'another-call-ongoing': + ToastUtils.pushedMissedCall(displayname); + break; + case 'not-approved': + ToastUtils.pushedMissedCallNotApproved(displayname); + break; + default: } await addMissedCallMessage(sender, incomingOfferTimestamp); diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx index 7f0c55a27..140469640 100644 --- a/ts/state/ducks/call.tsx +++ b/ts/state/ducks/call.tsx @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { setIsRinging } from '../../session/utils/RingingManager'; export type CallStatusEnum = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; @@ -31,11 +32,13 @@ const callSlice = createSlice({ } state.ongoingWith = callerPubkey; state.ongoingCallStatus = 'incoming'; + setIsRinging(true); return state; }, endCall(state: CallStateType) { state.ongoingCallStatus = undefined; state.ongoingWith = undefined; + setIsRinging(false); return state; }, @@ -50,6 +53,7 @@ const callSlice = createSlice({ } state.ongoingCallStatus = 'connecting'; state.callIsInFullScreen = false; + setIsRinging(false); return state; }, callConnected(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { @@ -66,6 +70,7 @@ const callSlice = createSlice({ ); return state; } + setIsRinging(false); state.ongoingCallStatus = 'ongoing'; state.callIsInFullScreen = false; @@ -80,6 +85,7 @@ const callSlice = createSlice({ window.log.warn('cannot start a call with an ongoing call already: ongoingCallStatus'); return state; } + setIsRinging(true); const callerPubkey = action.payload.pubkey; state.ongoingWith = callerPubkey; diff --git a/ts/state/ducks/section.tsx b/ts/state/ducks/section.tsx index e5332d8f8..43153a970 100644 --- a/ts/state/ducks/section.tsx +++ b/ts/state/ducks/section.tsx @@ -7,7 +7,6 @@ export enum SectionType { Profile, Message, Contact, - Channel, Settings, Moon, PathIndicator, diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index 57af32fdc..8f6fcd9f3 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -13,7 +13,7 @@ export interface UserConfigState { export const initialUserConfigState = { audioAutoplay: false, showRecoveryPhrasePrompt: true, - messageRequests: true, + messageRequests: false, }; const userConfigSlice = createSlice({ diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 9d15cc59d..dec166c96 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -539,6 +539,13 @@ export const getIsSelectedPrivate = createSelector( } ); +export const getIsSelectedBlocked = createSelector( + getConversationHeaderProps, + (headerProps): boolean => { + return headerProps?.isBlocked || false; + } +); + export const getIsSelectedNoteToSelf = createSelector( getConversationHeaderProps, (headerProps): boolean => { diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 75ff81dd3..148244bbd 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -94,6 +94,7 @@ export type LocalizerKeys = | 'pinConversation' | 'lightboxImageAlt' | 'linkDevice' + | 'callMissedNotApproved' | 'goToOurSurvey' | 'invalidPubkeyFormat' | 'disappearingMessagesDisabled' @@ -208,6 +209,7 @@ export type LocalizerKeys = | 'timerOption_0_seconds_abbreviated' | 'timerOption_5_minutes_abbreviated' | 'enterOptionalPassword' + | 'userRemovedFromModerators' | 'goToReleaseNotes' | 'unpinConversation' | 'viewMenuResetZoom' @@ -339,7 +341,7 @@ export type LocalizerKeys = | 'youDisabledDisappearingMessages' | 'updateGroupDialogTitle' | 'surveyTitle' - | 'userRemovedFromModerators' + | 'readReceiptDialogDescription' | 'timerOption_5_seconds' | 'failedToRemoveFromModerator' | 'conversationsHeader' diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 74cde12f0..218a4ac2a 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -148,10 +148,13 @@ async function createAccount(identityKeyPair: any) { await window.textsecure.storage.put('identityKey', identityKeyPair); await window.textsecure.storage.put('password', password); - await window.textsecure.storage.put('read-receipt-setting', false); + + // enable read-receipt by default + await window.textsecure.storage.put('read-receipt-setting', true); + await window.textsecure.storage.put('read-receipt-turn-on-asked', true); // this can be removed once enough people upgraded 8/12/2021 // Enable typing indicators by default - await window.textsecure.storage.put('typing-indicators-setting', Boolean(true)); + await window.textsecure.storage.put('typing-indicators-setting', true); await window.textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1); } diff --git a/yarn.lock b/yarn.lock index b66dbbada..654c33797 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1096,13 +1096,6 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/mkdirp@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f" - integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== - dependencies: - "@types/node" "*" - "@types/mocha@5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.0.0.tgz#a3014921991066193f6c8e47290d4d598dfd19e6" From 95e40c95094bd2237e227dd0c399e2b574dec1fb Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 8 Dec 2021 17:44:24 +1100 Subject: [PATCH 60/70] keep read-receipts disabled by default (#2071) --- _locales/en/messages.json | 1 - ts/components/session/ActionsPanel.tsx | 40 +++---------------- .../settings/section/CategoryPrivacy.tsx | 2 +- ts/types/LocalizerKeys.ts | 3 +- ts/util/accountManager.ts | 5 +-- 5 files changed, 9 insertions(+), 42 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 182541cc9..ad32e9b07 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -156,7 +156,6 @@ "spellCheckDirty": "You must restart Session to apply your new settings", "notifications": "Notifications", "readReceiptSettingDescription": "See and share when messages have been read (enables read receipts in all sessions).", - "readReceiptDialogDescription": "Read Receipts are now turned ON by default. Click \"Cancel\" to turn them down.", "readReceiptSettingTitle": "Read Receipts", "typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).", "typingIndicatorsSettingTitle": "Typing Indicators", diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index ef7800b1a..7ad2f323b 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { SessionIconButton } from './icon'; import { Avatar, AvatarSize } from '../Avatar'; import { SessionToastContainer } from './SessionToastContainer'; @@ -6,7 +6,6 @@ import { getConversationController } from '../../session/conversations'; import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; import { - createOrUpdateItem, generateAttachmentKeyIfEmpty, getAllOpenGroupV1Conversations, getItemById, @@ -37,11 +36,7 @@ import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; import { getSwarmPollingInstance } from '../../session/snode_api'; import { DURATION } from '../../session/constants'; import { conversationChanged, conversationRemoved } from '../../state/ducks/conversations'; -import { - editProfileModal, - onionPathModal, - updateConfirmModal, -} from '../../state/ducks/modalDialog'; +import { editProfileModal, onionPathModal } from '../../state/ducks/modalDialog'; import { uploadOurAvatar } from '../../interactions/conversationInteractions'; import { ModalContainer } from '../dialog/ModalContainer'; import { debounce } from 'underscore'; @@ -54,29 +49,7 @@ import { switchHtmlToDarkTheme, switchHtmlToLightTheme } from '../../state/ducks import { DraggableCallContainer } from './calling/DraggableCallContainer'; import { IncomingCallDialog } from './calling/IncomingCallDialog'; import { CallInFullScreenContainer } from './calling/CallInFullScreenContainer'; -import { SessionButtonColor } from './SessionButton'; -import { settingsReadReceipt } from './settings/section/CategoryPrivacy'; - -async function showTurnOnReadAck(dispatch: Dispatch) { - const singleShotSettingId = 'read-receipt-turn-on-asked'; - const item = (await getItemById(singleShotSettingId))?.value || false; - - if (!item) { - await createOrUpdateItem({ id: singleShotSettingId, value: true }); - // set it to true by default, user will be asked to willingfully turn it off - window.setSettingValue(settingsReadReceipt, true); - dispatch( - updateConfirmModal({ - title: window.i18n('readReceiptSettingTitle'), - messageSub: window.i18n('readReceiptDialogDescription'), - okTheme: SessionButtonColor.Green, - onClickCancel: () => { - window.setSettingValue(settingsReadReceipt, false); - }, - }) - ); - } -} + const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); const unreadMessageCount = useSelector(getUnreadMessageCount); @@ -254,7 +227,7 @@ const triggerAvatarReUploadIfNeeded = async () => { /** * This function is called only once: on app startup with a logged in user */ -const doAppStartUp = (dispatch: Dispatch) => { +const doAppStartUp = () => { // init the messageQueue. In the constructor, we add all not send messages // this call does nothing except calling the constructor, which will continue sending message in the pipeline void getMessageQueue().processAllPending(); @@ -273,8 +246,6 @@ const doAppStartUp = (dispatch: Dispatch) => { void loadDefaultRooms(); - void showTurnOnReadAck(dispatch); - debounce(triggerAvatarReUploadIfNeeded, 200); }; @@ -296,11 +267,10 @@ export const ActionsPanel = () => { const [startCleanUpMedia, setStartCleanUpMedia] = useState(false); const ourPrimaryConversation = useSelector(getOurPrimaryConversation); - const dispatch = useDispatch(); // this maxi useEffect is called only once: when the component is mounted. // For the action panel, it means this is called only one per app start/with a user loggedin useEffect(() => { - void doAppStartUp(dispatch); + void doAppStartUp(); }, []); // wait for cleanUpMediasInterval and then start cleaning up medias diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx index c6eea2df3..0ed432602 100644 --- a/ts/components/session/settings/section/CategoryPrivacy.tsx +++ b/ts/components/session/settings/section/CategoryPrivacy.tsx @@ -10,7 +10,7 @@ import { PasswordAction } from '../../../dialog/SessionPasswordDialog'; import { SessionButtonColor } from '../../SessionButton'; import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem'; -export const settingsReadReceipt = 'read-receipt-setting'; +const settingsReadReceipt = 'read-receipt-setting'; const settingsTypingIndicator = 'typing-indicators-setting'; const settingsAutoUpdate = 'auto-update'; diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 148244bbd..0cd644596 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -209,7 +209,6 @@ export type LocalizerKeys = | 'timerOption_0_seconds_abbreviated' | 'timerOption_5_minutes_abbreviated' | 'enterOptionalPassword' - | 'userRemovedFromModerators' | 'goToReleaseNotes' | 'unpinConversation' | 'viewMenuResetZoom' @@ -341,7 +340,7 @@ export type LocalizerKeys = | 'youDisabledDisappearingMessages' | 'updateGroupDialogTitle' | 'surveyTitle' - | 'readReceiptDialogDescription' + | 'userRemovedFromModerators' | 'timerOption_5_seconds' | 'failedToRemoveFromModerator' | 'conversationsHeader' diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 218a4ac2a..ecd2f5eaa 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -149,9 +149,8 @@ async function createAccount(identityKeyPair: any) { await window.textsecure.storage.put('identityKey', identityKeyPair); await window.textsecure.storage.put('password', password); - // enable read-receipt by default - await window.textsecure.storage.put('read-receipt-setting', true); - await window.textsecure.storage.put('read-receipt-turn-on-asked', true); // this can be removed once enough people upgraded 8/12/2021 + // disable read-receipt by default + await window.textsecure.storage.put('read-receipt-setting', false); // Enable typing indicators by default await window.textsecure.storage.put('typing-indicators-setting', true); From 28c7445dce1163c56da4625d7040980934728588 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 14 Dec 2021 15:15:12 +1100 Subject: [PATCH 61/70] refactor most of the components to outside of their Session folder (#2072) * refactor most of the components to outside of their Session folder * finish moving overlay and memberListItem to react hook * fix bug with kicked member len >2 not being displayed also sort admins first in UpdateGroupMembers dialog * fix admin leaving text of groupNotification * add a useFocusMount hook to focus input fields on mount * make click avatar convo item open only user dialog * cleanup config default.json * make sure to use convoController to build sync message * disable showing pubkey on opengroups * add a pause on audio playback Fixes #2079 --- Gruntfile.js | 10 +- _locales/en/messages.json | 2 +- config/default.json | 4 +- js/modules/signal.js | 8 +- main.js | 2 - password_preload.js | 2 +- preload.js | 4 +- stylesheets/_avatar.scss | 19 +- stylesheets/_modal.scss | 6 + stylesheets/_session.scss | 1 + stylesheets/_session_conversation.scss | 6 +- ts/components/CaptionEditor.tsx | 4 +- ts/components/MainViewController.tsx | 9 +- ts/components/MemberListItem.tsx | 43 ++ .../{session => }/SessionInboxView.tsx | 44 +- ts/components/SessionMainPanel.tsx | 2 +- .../{session => }/SessionPasswordPrompt.tsx | 4 +- .../{session => }/SessionScrollButton.tsx | 2 +- .../{session => }/SessionSearchInput.tsx | 2 +- .../{session => }/SessionToastContainer.tsx | 0 .../{session => }/SessionWrapperModal.tsx | 2 +- .../{session => }/SplitViewContainer.tsx | 0 ts/components/{ => avatar}/Avatar.tsx | 16 +- .../AvatarPlaceHolder/AvatarPlaceHolder.tsx | 2 +- .../AvatarPlaceHolder/ClosedGroupAvatar.tsx | 2 +- .../{session => basic}/PillDivider.tsx | 0 .../{session => basic}/SessionButton.tsx | 0 .../{session => basic}/SessionDropdown.tsx | 2 +- .../SessionDropdownItem.tsx | 3 +- .../SessionHTMLRenderer.tsx | 1 + ts/components/basic/SessionIdEditable.tsx | 54 ++ .../{session => basic}/SessionInput.tsx | 2 +- .../{session => basic}/SessionRadio.tsx | 0 .../{session => basic}/SessionRadioGroup.tsx | 0 .../{session => basic}/SessionSpinner.tsx | 0 .../{session => basic}/SessionToast.tsx | 2 +- .../{session => basic}/SessionToggle.tsx | 0 .../{session => }/calling/CallButtons.tsx | 8 +- .../calling/CallInFullScreenContainer.tsx | 6 +- .../calling/DraggableCallContainer.tsx | 10 +- .../calling/InConversationCallContainer.tsx | 14 +- .../calling/IncomingCallDialog.tsx | 14 +- .../conversation/ConversationHeader.tsx | 9 +- ts/components/conversation/ExpireTimer.tsx | 2 +- .../conversation/GroupNotification.tsx | 4 +- ts/components/conversation/H5AudioPlayer.tsx | 4 +- ts/components/conversation/ImageGrid.tsx | 2 +- .../conversation/SessionConversation.tsx | 42 +- .../conversation/SessionConversationDrafts.ts | 0 .../conversation/SessionEmojiPanel.tsx | 2 +- .../conversation/SessionFileDropzone.tsx | 2 +- .../conversation/SessionLastSeenIndicator.tsx | 0 .../conversation/SessionMessagesList.tsx | 21 +- .../SessionMessagesListContainer.tsx | 30 +- .../SessionQuotedMessageComposition.tsx | 12 +- .../conversation/SessionRecording.tsx | 6 +- .../conversation/SessionRightPanel.tsx | 35 +- .../conversation/SessionStagedLinkPreview.tsx | 8 +- .../conversation/StagedLinkPreview.tsx | 2 +- .../conversation/TimerNotification.tsx | 4 +- .../composition/CompositionBox.tsx | 64 +-- .../composition/CompositionButtons.tsx | 0 .../media-gallery/AttachmentSection.tsx | 2 +- .../media-gallery/DocumentListItem.tsx | 2 +- .../media-gallery/MediaGallery.tsx | 2 +- .../media-gallery/MediaGridItem.tsx | 4 +- .../media-gallery/groupMediaItemsByDate.ts | 3 +- .../ClickToTrustSender.tsx | 12 +- .../MessageAttachment.tsx | 22 +- .../MessageAuthorText.tsx | 10 +- .../{ => message-content}/MessageAvatar.tsx | 15 +- .../message-content}/MessageBody.tsx | 14 +- .../{ => message-content}/MessageContent.tsx | 10 +- .../MessageContentWithStatus.tsx | 9 +- .../MessageContextMenu.tsx | 34 +- .../{ => message-content}/MessagePreview.tsx | 14 +- .../{ => message-content}/MessageQuote.tsx | 11 +- .../{ => message-content}/MessageStatus.tsx | 4 +- .../{ => message-content}/MessageText.tsx | 8 +- .../OutgoingMessageStatus.tsx | 4 +- .../{ => message/message-content}/Quote.tsx | 20 +- .../DataExtractionNotification.tsx | 10 +- .../message/{ => message-item}/DateBreak.tsx | 0 .../GenericReadableMessage.tsx | 20 +- .../message-item}/GroupInvitation.tsx | 6 +- .../{ => message/message-item}/Message.tsx | 6 +- .../message-item}/MessageDetail.tsx | 10 +- .../message-item}/ReadableMessage.tsx | 12 +- .../notification-bubble/CallNotification.tsx | 13 +- .../NotificationBubble.tsx | 2 +- .../dialog/AdminLeaveClosedGroupDialog.tsx | 5 +- ts/components/dialog/DeleteAccountModal.tsx | 10 +- ts/components/dialog/EditProfileDialog.tsx | 16 +- ts/components/dialog/InviteContactsDialog.tsx | 281 +++++----- ts/components/dialog/ModeratorsAddDialog.tsx | 8 +- .../dialog/ModeratorsRemoveDialog.tsx | 319 ++++------- .../dialog/OnionStatusPathDialog.tsx | 6 +- ts/components/dialog/SessionConfirm.tsx | 10 +- ts/components/dialog/SessionModal.tsx | 5 +- .../dialog/SessionNicknameDialog.tsx | 4 +- .../dialog/SessionPasswordDialog.tsx | 5 +- ts/components/dialog/SessionSeedModal.tsx | 4 +- .../dialog/UpdateGroupMembersDialog.tsx | 509 ++++++++---------- .../dialog/UpdateGroupNameDialog.tsx | 6 +- ts/components/dialog/UserDetailsDialog.tsx | 9 +- .../icon/DropDownAndToggleButton.tsx | 0 ts/components/{session => }/icon/Icons.tsx | 11 +- .../{session => }/icon/SessionIcon.tsx | 0 .../{session => }/icon/SessionIconButton.tsx | 2 +- .../SessionNotificationCount.tsx | 0 ts/components/{session => }/icon/index.tsx | 0 .../{session => leftpane}/ActionsPanel.tsx | 21 +- .../{ => leftpane}/ContactListItem.tsx | 7 +- .../{ => leftpane}/ConversationListItem.tsx | 56 +- ts/components/{ => leftpane}/LeftPane.tsx | 24 +- .../LeftPaneContactSection.tsx | 4 +- .../leftpane/LeftPaneMessageSection.tsx | 220 ++++++++ .../LeftPaneSectionHeader.tsx | 4 +- .../LeftPaneSettingSection.tsx | 7 +- .../MessageRequestsBanner.tsx | 2 +- .../leftpane/overlay/OverlayClosedGroup.tsx | 120 +++++ .../leftpane/overlay/OverlayHeader.tsx | 33 ++ .../leftpane/overlay/OverlayMessage.tsx | 101 ++++ .../overlay/OverlayMessageRequest.tsx | 120 +++++ .../leftpane/overlay/OverlayOpenGroup.tsx | 89 +++ .../overlay}/SessionJoinableDefaultRooms.tsx | 21 +- ts/components/{ => lightbox}/Lightbox.tsx | 14 +- .../{ => lightbox}/LightboxGallery.tsx | 10 +- .../menu/ConversationHeaderMenu.tsx | 2 +- .../menu/ConversationListItemContextMenu.tsx | 4 +- ts/components/{session => }/menu/Menu.tsx | 36 +- .../{session => registration}/AccentText.tsx | 0 .../registration/RegistrationStages.tsx | 18 +- .../registration/RegistrationUserDetails.tsx | 2 +- .../SessionRegistrationView.tsx | 8 +- .../{session => }/registration/SignInTab.tsx | 8 +- .../{session => }/registration/SignUpTab.tsx | 6 +- .../registration/TermsAndConditions.tsx | 2 +- ts/components/{ => search}/SearchResults.tsx | 2 +- .../{ => search}/UserSearchResults.tsx | 4 +- .../session/LeftPaneMessageSection.tsx | 477 ---------------- .../session/SessionClosableOverlay.tsx | 317 ----------- ts/components/session/SessionIdEditable.tsx | 73 --- .../session/SessionMemberListItem.tsx | 63 --- .../settings/BlockedUserSettings.tsx | 9 +- .../SessionNotificationGroupSettings.tsx | 2 +- .../settings/SessionSettingListItem.tsx | 7 +- .../settings/SessionSettings.tsx | 8 +- .../settings/SessionSettingsHeader.tsx | 0 .../settings/ZoomingSessionSlider.tsx | 0 .../settings/section/CategoryAppearance.tsx | 13 +- .../settings/section/CategoryPrivacy.tsx | 13 +- ts/data/opengroups.ts | 4 +- ts/hooks/useFocusMount.ts | 11 + ts/hooks/useParamSelector.ts | 30 ++ ts/hooks/useSet.ts | 27 + ts/hooks/useWeAreAdmin.ts | 14 + ts/interactions/conversationInteractions.ts | 20 +- .../conversations/unsendingInteractions.ts | 6 +- ts/interactions/messageInteractions.ts | 9 +- ts/models/conversation.ts | 36 +- ts/models/message.ts | 3 +- ts/receiver/attachments.ts | 6 +- ts/receiver/callMessage.ts | 2 +- ts/receiver/closedGroups.ts | 9 +- ts/receiver/configMessage.ts | 4 +- ts/receiver/contentMessage.ts | 2 - ts/receiver/receiver.ts | 8 +- .../apis/file_server_api}/FileServerApiV2.ts | 6 +- .../apis/file_server_api}/index.ts | 0 .../open_group_api}/opengroupV2/ApiAuth.ts | 8 +- .../open_group_api}/opengroupV2/ApiUtil.ts | 13 +- .../opengroupV2/JoinOpenGroupV2.ts | 8 +- .../opengroupV2/OpenGroupAPIV2.ts | 10 +- .../opengroupV2/OpenGroupAPIV2CompactPoll.ts | 6 +- .../opengroupV2/OpenGroupAPIV2Parser.ts | 0 .../opengroupV2/OpenGroupManagerV2.ts | 8 +- .../opengroupV2/OpenGroupMessageV2.ts | 4 +- .../opengroupV2/OpenGroupServerPoller.ts | 14 +- .../opengroupV2/OpenGroupUpdate.ts | 14 +- .../apis/open_group_api}/opengroupV2/index.ts | 0 .../open_group_api}/utils/OpenGroupUtils.ts | 2 +- .../apis/open_group_api}/utils/index.ts | 0 .../apis/push_notification_api}/PnServer.ts | 2 +- .../apis/push_notification_api}/index.ts | 0 .../{ => apis}/seed_node_api/SeedNodeAPI.ts | 6 +- ts/session/{ => apis}/seed_node_api/index.ts | 0 ts/session/{ => apis}/snode_api/SNodeAPI.ts | 12 +- ts/session/{ => apis}/snode_api/index.ts | 0 ts/session/{ => apis}/snode_api/lokiRpc.ts | 4 +- ts/session/{ => apis}/snode_api/onions.ts | 10 +- ts/session/{ => apis}/snode_api/snodePool.ts | 6 +- .../{ => apis}/snode_api/swarmPolling.ts | 24 +- .../conversations/ConversationController.ts | 9 +- ts/session/group/index.ts | 32 +- ts/session/onions/onionPath.ts | 6 +- ts/session/onions/onionSend.ts | 2 +- ts/session/sending/MessageQueue.ts | 2 +- ts/session/sending/MessageSender.ts | 12 +- ts/session/sending/MessageSentHandler.ts | 2 +- ts/session/utils/Attachments.ts | 2 +- ts/session/utils/AttachmentsV2.ts | 4 +- ts/session/utils/Toast.tsx | 6 +- ts/session/utils/calling/CallManager.ts | 4 +- ts/session/utils/index.ts | 2 - ts/session/utils/syncUtils.ts | 7 +- ts/state/ducks/conversations.ts | 7 +- ts/state/ducks/defaultRooms.tsx | 2 +- ts/state/ducks/index.ts | 27 + ts/state/ducks/section.tsx | 31 +- ts/state/ducks/stagedAttachments.ts | 2 +- ts/state/selectors/conversations.ts | 65 ++- ts/state/selectors/index.ts | 25 + ts/state/selectors/section.ts | 9 +- ts/state/selectors/stagedAttachments.ts | 2 +- ts/state/smart/SessionConversation.ts | 2 +- .../media-gallery/groupMessagesByDate_test.ts | 2 +- ts/test/session/unit/onion/GuardNodes_test.ts | 4 +- .../session/unit/onion/OnionErrors_test.ts | 6 +- ts/test/session/unit/onion/OnionPaths_test.ts | 4 +- .../session/unit/onion/SeedNodeAPI_test.ts | 6 +- .../unit/onion/SnodePoolUpdate_test.ts | 4 +- .../unit/sending/MessageSender_test.ts | 4 +- .../unit/swarm_polling/SwarmPolling_test.ts | 4 +- ts/test/test-utils/utils/message.ts | 6 +- ts/types/index.ts | 3 + ts/util/accountManager.ts | 2 + ts/util/attachmentsUtil.ts | 2 +- ts/util/timer.ts | 2 +- tslint.json | 1 + 230 files changed, 2246 insertions(+), 2391 deletions(-) create mode 100644 ts/components/MemberListItem.tsx rename ts/components/{session => }/SessionInboxView.tsx (74%) rename ts/components/{session => }/SessionPasswordPrompt.tsx (98%) rename ts/components/{session => }/SessionScrollButton.tsx (89%) rename ts/components/{session => }/SessionSearchInput.tsx (90%) rename ts/components/{session => }/SessionToastContainer.tsx (100%) rename ts/components/{session => }/SessionWrapperModal.tsx (98%) rename ts/components/{session => }/SplitViewContainer.tsx (100%) rename ts/components/{ => avatar}/Avatar.tsx (91%) rename ts/components/{ => avatar}/AvatarPlaceHolder/AvatarPlaceHolder.tsx (98%) rename ts/components/{ => avatar}/AvatarPlaceHolder/ClosedGroupAvatar.tsx (94%) rename ts/components/{session => basic}/PillDivider.tsx (100%) rename ts/components/{session => basic}/SessionButton.tsx (100%) rename ts/components/{session => basic}/SessionDropdown.tsx (96%) rename ts/components/{session => basic}/SessionDropdownItem.tsx (93%) rename ts/components/{session => basic}/SessionHTMLRenderer.tsx (91%) create mode 100644 ts/components/basic/SessionIdEditable.tsx rename ts/components/{session => basic}/SessionInput.tsx (98%) rename ts/components/{session => basic}/SessionRadio.tsx (100%) rename ts/components/{session => basic}/SessionRadioGroup.tsx (100%) rename ts/components/{session => basic}/SessionSpinner.tsx (100%) rename ts/components/{session => basic}/SessionToast.tsx (97%) rename ts/components/{session => basic}/SessionToggle.tsx (100%) rename ts/components/{session => }/calling/CallButtons.tsx (97%) rename ts/components/{session => }/calling/CallInFullScreenContainer.tsx (93%) rename ts/components/{session => }/calling/DraggableCallContainer.tsx (93%) rename ts/components/{session => }/calling/InConversationCallContainer.tsx (93%) rename ts/components/{session => }/calling/IncomingCallDialog.tsx (86%) rename ts/components/{session => }/conversation/SessionConversation.tsx (93%) rename ts/components/{session => }/conversation/SessionConversationDrafts.ts (100%) rename ts/components/{session => }/conversation/SessionEmojiPanel.tsx (93%) rename ts/components/{session => }/conversation/SessionFileDropzone.tsx (95%) rename ts/components/{session => }/conversation/SessionLastSeenIndicator.tsx (100%) rename ts/components/{session => }/conversation/SessionMessagesList.tsx (83%) rename ts/components/{session => }/conversation/SessionMessagesListContainer.tsx (95%) rename ts/components/{session => }/conversation/SessionQuotedMessageComposition.tsx (89%) rename ts/components/{session => }/conversation/SessionRecording.tsx (98%) rename ts/components/{session => }/conversation/SessionRightPanel.tsx (92%) rename ts/components/{session => }/conversation/SessionStagedLinkPreview.tsx (92%) rename ts/components/{session => }/conversation/composition/CompositionBox.tsx (95%) rename ts/components/{session => }/conversation/composition/CompositionButtons.tsx (100%) rename ts/components/conversation/message/{ => message-content}/ClickToTrustSender.tsx (91%) rename ts/components/conversation/message/{ => message-content}/MessageAttachment.tsx (91%) rename ts/components/conversation/message/{ => message-content}/MessageAuthorText.tsx (84%) rename ts/components/conversation/message/{ => message-content}/MessageAvatar.tsx (77%) rename ts/components/conversation/{ => message/message-content}/MessageBody.tsx (90%) rename ts/components/conversation/message/{ => message-content}/MessageContent.tsx (96%) rename ts/components/conversation/message/{ => message-content}/MessageContentWithStatus.tsx (92%) rename ts/components/conversation/message/{ => message-content}/MessageContextMenu.tsx (88%) rename ts/components/conversation/message/{ => message-content}/MessagePreview.tsx (86%) rename ts/components/conversation/message/{ => message-content}/MessageQuote.tsx (90%) rename ts/components/conversation/message/{ => message-content}/MessageStatus.tsx (83%) rename ts/components/conversation/message/{ => message-content}/MessageText.tsx (86%) rename ts/components/conversation/message/{ => message-content}/OutgoingMessageStatus.tsx (93%) rename ts/components/conversation/{ => message/message-content}/Quote.tsx (95%) rename ts/components/conversation/{ => message/message-item}/DataExtractionNotification.tsx (78%) rename ts/components/conversation/message/{ => message-item}/DateBreak.tsx (100%) rename ts/components/conversation/message/{ => message-item}/GenericReadableMessage.tsx (89%) rename ts/components/conversation/{ => message/message-item}/GroupInvitation.tsx (86%) rename ts/components/conversation/{ => message/message-item}/Message.tsx (81%) rename ts/components/conversation/{ => message/message-item}/MessageDetail.tsx (92%) rename ts/components/conversation/{ => message/message-item}/ReadableMessage.tsx (92%) rename ts/components/conversation/{ => message/message-item}/notification-bubble/CallNotification.tsx (82%) rename ts/components/conversation/{ => message/message-item}/notification-bubble/NotificationBubble.tsx (94%) rename ts/components/{session => }/icon/DropDownAndToggleButton.tsx (100%) rename ts/components/{session => }/icon/Icons.tsx (99%) rename ts/components/{session => }/icon/SessionIcon.tsx (100%) rename ts/components/{session => }/icon/SessionIconButton.tsx (96%) rename ts/components/{session => icon}/SessionNotificationCount.tsx (100%) rename ts/components/{session => }/icon/index.tsx (100%) rename ts/components/{session => leftpane}/ActionsPanel.tsx (93%) rename ts/components/{ => leftpane}/ContactListItem.tsx (84%) rename ts/components/{ => leftpane}/ConversationListItem.tsx (87%) rename ts/components/{ => leftpane}/LeftPane.tsx (67%) rename ts/components/{session => leftpane}/LeftPaneContactSection.tsx (92%) create mode 100644 ts/components/leftpane/LeftPaneMessageSection.tsx rename ts/components/{session => leftpane}/LeftPaneSectionHeader.tsx (97%) rename ts/components/{session => leftpane}/LeftPaneSettingSection.tsx (95%) rename ts/components/{session => leftpane}/MessageRequestsBanner.tsx (99%) create mode 100644 ts/components/leftpane/overlay/OverlayClosedGroup.tsx create mode 100644 ts/components/leftpane/overlay/OverlayHeader.tsx create mode 100644 ts/components/leftpane/overlay/OverlayMessage.tsx create mode 100644 ts/components/leftpane/overlay/OverlayMessageRequest.tsx create mode 100644 ts/components/leftpane/overlay/OverlayOpenGroup.tsx rename ts/components/{session => leftpane/overlay}/SessionJoinableDefaultRooms.tsx (88%) rename ts/components/{ => lightbox}/Lightbox.tsx (95%) rename ts/components/{ => lightbox}/LightboxGallery.tsx (89%) rename ts/components/{session => }/menu/ConversationHeaderMenu.tsx (97%) rename ts/components/{session => }/menu/ConversationListItemContextMenu.tsx (95%) rename ts/components/{session => }/menu/Menu.tsx (95%) rename ts/components/{session => registration}/AccentText.tsx (100%) rename ts/components/{session => }/registration/RegistrationStages.tsx (93%) rename ts/components/{session => }/registration/RegistrationUserDetails.tsx (98%) rename ts/components/{session => registration}/SessionRegistrationView.tsx (86%) rename ts/components/{session => }/registration/SignInTab.tsx (97%) rename ts/components/{session => }/registration/SignUpTab.tsx (97%) rename ts/components/{session => }/registration/TermsAndConditions.tsx (77%) rename ts/components/{ => search}/SearchResults.tsx (98%) rename ts/components/{ => search}/UserSearchResults.tsx (92%) delete mode 100644 ts/components/session/LeftPaneMessageSection.tsx delete mode 100644 ts/components/session/SessionClosableOverlay.tsx delete mode 100644 ts/components/session/SessionIdEditable.tsx delete mode 100644 ts/components/session/SessionMemberListItem.tsx rename ts/components/{session => }/settings/BlockedUserSettings.tsx (80%) rename ts/components/{session => }/settings/SessionNotificationGroupSettings.tsx (94%) rename ts/components/{session => }/settings/SessionSettingListItem.tsx (91%) rename ts/components/{session => }/settings/SessionSettings.tsx (97%) rename ts/components/{session => }/settings/SessionSettingsHeader.tsx (100%) rename ts/components/{session => }/settings/ZoomingSessionSlider.tsx (100%) rename ts/components/{session => }/settings/section/CategoryAppearance.tsx (93%) rename ts/components/{session => }/settings/section/CategoryPrivacy.tsx (92%) create mode 100644 ts/hooks/useFocusMount.ts create mode 100644 ts/hooks/useSet.ts create mode 100644 ts/hooks/useWeAreAdmin.ts rename ts/{fileserver => session/apis/file_server_api}/FileServerApiV2.ts (95%) rename ts/{fileserver => session/apis/file_server_api}/index.ts (100%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/ApiAuth.ts (97%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/ApiUtil.ts (93%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/JoinOpenGroupV2.ts (96%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupAPIV2.ts (98%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupAPIV2CompactPoll.ts (98%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupAPIV2Parser.ts (100%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupManagerV2.ts (96%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupMessageV2.ts (95%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupServerPoller.ts (97%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/OpenGroupUpdate.ts (84%) rename ts/{opengroup => session/apis/open_group_api}/opengroupV2/index.ts (100%) rename ts/{opengroup => session/apis/open_group_api}/utils/OpenGroupUtils.ts (98%) rename ts/{opengroup => session/apis/open_group_api}/utils/index.ts (100%) rename ts/{pushnotification => session/apis/push_notification_api}/PnServer.ts (96%) rename ts/{pushnotification => session/apis/push_notification_api}/index.ts (100%) rename ts/session/{ => apis}/seed_node_api/SeedNodeAPI.ts (98%) rename ts/session/{ => apis}/seed_node_api/index.ts (100%) rename ts/session/{ => apis}/snode_api/SNodeAPI.ts (99%) rename ts/session/{ => apis}/snode_api/index.ts (100%) rename ts/session/{ => apis}/snode_api/lokiRpc.ts (97%) rename ts/session/{ => apis}/snode_api/onions.ts (98%) rename ts/session/{ => apis}/snode_api/snodePool.ts (98%) rename ts/session/{ => apis}/snode_api/swarmPolling.ts (95%) create mode 100644 ts/state/ducks/index.ts create mode 100644 ts/state/selectors/index.ts create mode 100644 ts/types/index.ts diff --git a/Gruntfile.js b/Gruntfile.js index 365942e8f..76e83b0db 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -151,14 +151,6 @@ module.exports = grunt => { grunt.file.write(configPath, `${JSON.stringify(localConfig)}\n`); } - grunt.registerTask('getExpireTime', () => { - grunt.task.requires('gitinfo'); - const gitinfo = grunt.config.get('gitinfo'); - const committed = gitinfo.local.branch.current.lastCommitTime; - const time = Date.parse(committed) + 1000 * 60 * 60 * 24 * 90; - updateLocalConfig({ buildExpiration: time }); - }); - grunt.registerTask('getCommitHash', () => { grunt.task.requires('gitinfo'); const gitinfo = grunt.config.get('gitinfo'); @@ -167,7 +159,7 @@ module.exports = grunt => { }); grunt.registerTask('dev', ['default', 'watch']); - grunt.registerTask('date', ['gitinfo', 'getExpireTime']); + grunt.registerTask('date', ['gitinfo']); grunt.registerTask('default', [ 'exec:build-protobuf', 'exec:transpile', diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ad32e9b07..7f08d23c9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -327,7 +327,7 @@ "addAsModerator": "Add as Moderator", "removeFromModerators": "Remove From Moderators", "add": "Add", - "addingContacts": "Adding contacts to", + "addingContacts": "Adding contacts to $name$", "noContactsToAdd": "No contacts to add", "noMembersInThisGroup": "No other members in this group", "noModeratorsToRemove": "no moderators to remove", diff --git a/config/default.json b/config/default.json index 8a3b35bb1..bb1eab171 100644 --- a/config/default.json +++ b/config/default.json @@ -16,8 +16,6 @@ ], "updatesEnabled": false, "openDevTools": false, - "buildExpiration": 0, "commitHash": "", - "import": false, - "serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx" + "import": false } diff --git a/js/modules/signal.js b/js/modules/signal.js index 282e18487..ecd4a9b01 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -8,12 +8,14 @@ const OS = require('../../ts/OS'); const Settings = require('./settings'); const Util = require('../../ts/util'); const LinkPreviews = require('./link_previews'); -const { Message } = require('../../ts/components/conversation/Message'); +const { Message } = require('../../ts/components/conversation/message/message-item/Message'); // Components -const { SessionRegistrationView } = require('../../ts/components/session/SessionRegistrationView'); +const { + SessionRegistrationView, +} = require('../../ts/components/registration/SessionRegistrationView'); -const { SessionInboxView } = require('../../ts/components/session/SessionInboxView'); +const { SessionInboxView } = require('../../ts/components/SessionInboxView'); // Types const AttachmentType = require('./types/attachment'); diff --git a/main.js b/main.js index 307069835..f636f636e 100644 --- a/main.js +++ b/main.js @@ -154,7 +154,6 @@ function prepareURL(pathSegments, moreKeys) { name: packageJson.productName, locale: locale.name, version: app.getVersion(), - buildExpiration: config.get('buildExpiration'), commitHash: config.get('commitHash'), serverUrl: config.get('serverUrl'), localUrl: config.get('localUrl'), @@ -167,7 +166,6 @@ function prepareURL(pathSegments, moreKeys) { appInstance: process.env.NODE_APP_INSTANCE, proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy, contentProxyUrl: config.contentProxyUrl, - serverTrustRoot: config.get('serverTrustRoot'), appStartInitialSpellcheckSetting, ...moreKeys, }, diff --git a/password_preload.js b/password_preload.js index 2c064ba98..dac1e01d8 100644 --- a/password_preload.js +++ b/password_preload.js @@ -21,7 +21,7 @@ window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; -const { SessionPasswordPrompt } = require('./ts/components/session/SessionPasswordPrompt'); +const { SessionPasswordPrompt } = require('./ts/components/SessionPasswordPrompt'); window.Signal = { Components: { diff --git a/preload.js b/preload.js index e53586ed7..115b2150a 100644 --- a/preload.js +++ b/preload.js @@ -30,11 +30,9 @@ window.getEnvironment = () => config.environment; window.getAppInstance = () => config.appInstance; window.getVersion = () => config.version; window.isDev = () => config.environment === 'development'; -window.getExpiration = () => config.buildExpiration; window.getCommitHash = () => config.commitHash; window.getNodeVersion = () => config.node_version; window.getHostName = () => config.hostname; -window.getServerTrustRoot = () => config.serverTrustRoot; window.isBehindProxy = () => Boolean(config.proxyUrl); window.lokiFeatureFlags = { @@ -214,7 +212,7 @@ window.Signal = Signal.setup({ logger: window.log, }); -window.getSwarmPollingInstance = require('./ts/session/snode_api/').getSwarmPollingInstance; +window.getSwarmPollingInstance = require('./ts/session/apis/snode_api/').getSwarmPollingInstance; const WorkerInterface = require('./js/modules/util_worker_interface'); diff --git a/stylesheets/_avatar.scss b/stylesheets/_avatar.scss index 04a2f9978..b7abb7f64 100644 --- a/stylesheets/_avatar.scss +++ b/stylesheets/_avatar.scss @@ -8,6 +8,7 @@ $borderAvatarColor: unquote( vertical-align: middle; display: inline-block; border-radius: 50%; + flex-shrink: 0; img { object-fit: cover; @@ -16,21 +17,6 @@ $borderAvatarColor: unquote( } } -.module-avatar__label { - width: 100%; - text-align: center; - font-weight: 300; - text-transform: uppercase; - color: $color-white; -} - -.module-avatar__icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - .module-avatar__icon--crown-wrapper { position: absolute; bottom: 0%; @@ -129,7 +115,8 @@ $borderAvatarColor: unquote( .module-avatar-clickable { transition: $session-transition-duration; + cursor: pointer; &:hover { - opacity: $session-subtle-factor; + filter: grayscale(0.7); } } diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index 2387092a6..4f0de6cb8 100644 --- a/stylesheets/_modal.scss +++ b/stylesheets/_modal.scss @@ -26,6 +26,12 @@ padding: 1.1em; } +.session-modal { + .contact-selection-list { + width: 100%; + } +} + .create-group-dialog, .add-moderators-dialog, .remove-moderators-dialog, diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index b60aa76a2..c16bc8a07 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -494,6 +494,7 @@ label { .group-member-list__selection { overflow-y: auto; + width: 100%; } &__centered { diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index a48783836..bcc4fbcfe 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -158,10 +158,8 @@ z-index: 1; .session-icon-button { - // & > .session-icon-button { margin-right: $session-margin-sm; - } - .session-icon-button { + display: flex; justify-content: center; align-items: center; @@ -169,7 +167,7 @@ &:hover { opacity: 1; - transform: scale(0.93); + filter: brightness(0.9); transition: $session-transition-duration; } diff --git a/ts/components/CaptionEditor.tsx b/ts/components/CaptionEditor.tsx index 68d6eae5a..4e842e77b 100644 --- a/ts/components/CaptionEditor.tsx +++ b/ts/components/CaptionEditor.tsx @@ -5,9 +5,9 @@ import * as GoogleChrome from '../util/GoogleChrome'; import { AttachmentType } from '../types/Attachment'; -import { SessionInput } from './session/SessionInput'; -import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton'; import autoBind from 'auto-bind'; +import { SessionButton, SessionButtonColor, SessionButtonType } from './basic/SessionButton'; +import { SessionInput } from './basic/SessionInput'; interface Props { attachment: AttachmentType; diff --git a/ts/components/MainViewController.tsx b/ts/components/MainViewController.tsx index 9c222c93f..dcbf8c278 100644 --- a/ts/components/MainViewController.tsx +++ b/ts/components/MainViewController.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { ContactType } from './session/SessionMemberListItem'; import { ToastUtils } from '../session/utils'; import { createClosedGroup as createClosedGroupV2 } from '../receiver/closedGroups'; import { VALIDATION } from '../session/constants'; @@ -38,7 +37,7 @@ export class MessageView extends React.Component { */ async function createClosedGroup( groupName: string, - groupMembers: Array + groupMemberIds: Array ): Promise { // Validate groupName and groupMembers length if (groupName.length === 0) { @@ -53,16 +52,14 @@ async function createClosedGroup( // >= because we add ourself as a member AFTER this. so a 10 group is already invalid as it will be 11 with ourself // the same is valid with groups count < 1 - if (groupMembers.length < 1) { + if (groupMemberIds.length < 1) { ToastUtils.pushToastError('pickClosedGroupMember', window.i18n('pickClosedGroupMember')); return false; - } else if (groupMembers.length >= VALIDATION.CLOSED_GROUP_SIZE_LIMIT) { + } else if (groupMemberIds.length >= VALIDATION.CLOSED_GROUP_SIZE_LIMIT) { ToastUtils.pushToastError('closedGroupMaxSize', window.i18n('closedGroupMaxSize')); return false; } - const groupMemberIds = groupMembers.map(m => m.id); - await createClosedGroupV2(groupName, groupMemberIds); return true; diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx new file mode 100644 index 000000000..79e56532f --- /dev/null +++ b/ts/components/MemberListItem.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Avatar, AvatarSize } from './avatar/Avatar'; +import { Constants } from '../session'; +import { SessionIcon } from './icon'; +import { useConversationUsernameOrShorten } from '../hooks/useParamSelector'; + +const AvatarItem = (props: { memberPubkey: string }) => { + return ; +}; + +export const MemberListItem = (props: { + pubkey: string; + isSelected: boolean; + // this bool is used to make a zombie appear with less opacity than a normal member + isZombie?: boolean; + onSelect?: (pubkey: string) => void; + onUnselect?: (pubkey: string) => void; +}) => { + const { isSelected, pubkey, isZombie, onSelect, onUnselect } = props; + + const memberName = useConversationUsernameOrShorten(pubkey); + + return ( +
{ + isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey); + }} + role="button" + > +
+ + + + {memberName} +
+ + + +
+ ); +}; diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/SessionInboxView.tsx similarity index 74% rename from ts/components/session/SessionInboxView.tsx rename to ts/components/SessionInboxView.tsx index 57fb092c9..6a58f07f2 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/SessionInboxView.tsx @@ -1,32 +1,32 @@ import React from 'react'; import { Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { getConversationController } from '../../session/conversations'; -import { UserUtils } from '../../session/utils'; -import { createStore } from '../../state/createStore'; -import { - actions as conversationActions, - getEmptyConversationState, - openConversationWithMessages, -} from '../../state/ducks/conversations'; -import { initialDefaultRoomState } from '../../state/ducks/defaultRooms'; -import { initialModalState } from '../../state/ducks/modalDialog'; -import { initialOnionPathState } from '../../state/ducks/onion'; -import { initialSearchState } from '../../state/ducks/search'; -import { initialSectionState } from '../../state/ducks/section'; -import { initialThemeState } from '../../state/ducks/theme'; -import { initialUserConfigState } from '../../state/ducks/userConfig'; -import { StateType } from '../../state/reducer'; -import { makeLookup } from '../../util'; -import { LeftPane } from '../LeftPane'; -import { SessionMainPanel } from '../SessionMainPanel'; +import { LeftPane } from './leftpane/LeftPane'; // tslint:disable-next-line: no-submodule-imports import { PersistGate } from 'redux-persist/integration/react'; import { persistStore } from 'redux-persist'; -import { TimerOptionsArray } from '../../state/ducks/timerOptions'; -import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments'; -import { initialCallState } from '../../state/ducks/call'; +import { getConversationController } from '../session/conversations'; +import { UserUtils } from '../session/utils'; +import { initialCallState } from '../state/ducks/call'; +import { + actions as conversationActions, + getEmptyConversationState, + openConversationWithMessages, +} from '../state/ducks/conversations'; +import { initialDefaultRoomState } from '../state/ducks/defaultRooms'; +import { initialModalState } from '../state/ducks/modalDialog'; +import { initialOnionPathState } from '../state/ducks/onion'; +import { initialSearchState } from '../state/ducks/search'; +import { initialSectionState } from '../state/ducks/section'; +import { getEmptyStagedAttachmentsState } from '../state/ducks/stagedAttachments'; +import { initialThemeState } from '../state/ducks/theme'; +import { TimerOptionsArray } from '../state/ducks/timerOptions'; +import { initialUserConfigState } from '../state/ducks/userConfig'; +import { StateType } from '../state/reducer'; +import { makeLookup } from '../util'; +import { SessionMainPanel } from './SessionMainPanel'; +import { createStore } from '../state/createStore'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 diff --git a/ts/components/SessionMainPanel.tsx b/ts/components/SessionMainPanel.tsx index 47e05a9a4..035275712 100644 --- a/ts/components/SessionMainPanel.tsx +++ b/ts/components/SessionMainPanel.tsx @@ -4,7 +4,7 @@ import { useAppIsFocused } from '../hooks/useAppFocused'; import { getFocusedSettingsSection } from '../state/selectors/section'; import { SmartSessionConversation } from '../state/smart/SessionConversation'; -import { SessionSettingsView } from './session/settings/SessionSettings'; +import { SessionSettingsView } from './settings/SessionSettings'; const FilteredSettingsView = SessionSettingsView as any; diff --git a/ts/components/session/SessionPasswordPrompt.tsx b/ts/components/SessionPasswordPrompt.tsx similarity index 98% rename from ts/components/session/SessionPasswordPrompt.tsx rename to ts/components/SessionPasswordPrompt.tsx index a4c9bbeef..5b5333e44 100644 --- a/ts/components/session/SessionPasswordPrompt.tsx +++ b/ts/components/SessionPasswordPrompt.tsx @@ -2,10 +2,10 @@ import React from 'react'; import classNames from 'classnames'; import { SessionIcon } from './icon'; -import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton'; -import { Constants } from '../../session'; import { withTheme } from 'styled-components'; import autoBind from 'auto-bind'; +import { SessionButton, SessionButtonColor, SessionButtonType } from './basic/SessionButton'; +import { Constants } from '../session'; interface State { error: string; diff --git a/ts/components/session/SessionScrollButton.tsx b/ts/components/SessionScrollButton.tsx similarity index 89% rename from ts/components/session/SessionScrollButton.tsx rename to ts/components/SessionScrollButton.tsx index 20cf7d0f7..314396f05 100644 --- a/ts/components/session/SessionScrollButton.tsx +++ b/ts/components/SessionScrollButton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { getShowScrollButton } from '../../state/selectors/conversations'; +import { getShowScrollButton } from '../state/selectors/conversations'; import { SessionIconButton } from './icon'; diff --git a/ts/components/session/SessionSearchInput.tsx b/ts/components/SessionSearchInput.tsx similarity index 90% rename from ts/components/session/SessionSearchInput.tsx rename to ts/components/SessionSearchInput.tsx index 3c25f4301..1ed173796 100644 --- a/ts/components/session/SessionSearchInput.tsx +++ b/ts/components/SessionSearchInput.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { getConversationsCount } from '../../state/selectors/conversations'; +import { getConversationsCount } from '../state/selectors/conversations'; import { SessionIconButton } from './icon'; type Props = { diff --git a/ts/components/session/SessionToastContainer.tsx b/ts/components/SessionToastContainer.tsx similarity index 100% rename from ts/components/session/SessionToastContainer.tsx rename to ts/components/SessionToastContainer.tsx diff --git a/ts/components/session/SessionWrapperModal.tsx b/ts/components/SessionWrapperModal.tsx similarity index 98% rename from ts/components/session/SessionWrapperModal.tsx rename to ts/components/SessionWrapperModal.tsx index ff615e5c6..f34453342 100644 --- a/ts/components/session/SessionWrapperModal.tsx +++ b/ts/components/SessionWrapperModal.tsx @@ -2,10 +2,10 @@ import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { SessionIconButton } from './icon/'; -import { SessionButton } from './SessionButton'; // tslint:disable-next-line: no-submodule-imports import useKey from 'react-use/lib/useKey'; +import { SessionButton } from './basic/SessionButton'; export type SessionWrapperModalType = { title?: string; diff --git a/ts/components/session/SplitViewContainer.tsx b/ts/components/SplitViewContainer.tsx similarity index 100% rename from ts/components/session/SplitViewContainer.tsx rename to ts/components/SplitViewContainer.tsx diff --git a/ts/components/Avatar.tsx b/ts/components/avatar/Avatar.tsx similarity index 91% rename from ts/components/Avatar.tsx rename to ts/components/avatar/Avatar.tsx index 4af7040f9..1b1da7c76 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/avatar/Avatar.tsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; import classNames from 'classnames'; -import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; +import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; import _ from 'underscore'; import { useAvatarPath, useConversationUsername, useIsClosedGroup, -} from '../hooks/useParamSelector'; +} from '../../hooks/useParamSelector'; import { AvatarPlaceHolder } from './AvatarPlaceHolder/AvatarPlaceHolder'; import { ClosedGroupAvatar } from './AvatarPlaceHolder/ClosedGroupAvatar'; -import { useDisableDrag } from '../hooks/useDisableDrag'; +import { useDisableDrag } from '../../hooks/useDisableDrag'; export enum AvatarSize { XS = 28, @@ -111,10 +111,12 @@ const AvatarInner = (props: Props) => { hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image', isClickable && 'module-avatar-clickable' )} - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - props.onAvatarClick?.(); + onMouseDown={e => { + if (props.onAvatarClick) { + e.stopPropagation(); + e.preventDefault(); + props.onAvatarClick?.(); + } }} role="button" data-testid={dataTestId} diff --git a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx similarity index 98% rename from ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx rename to ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx index 2aa30a550..3136d728c 100644 --- a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx +++ b/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { getInitials } from '../../util/getInitials'; +import { getInitials } from '../../../util/getInitials'; type Props = { diameter: number; diff --git a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx similarity index 94% rename from ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx rename to ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx index c54fee35f..486993064 100644 --- a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx +++ b/ts/components/avatar/AvatarPlaceHolder/ClosedGroupAvatar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useMembersAvatars } from '../../hooks/useMembersAvatars'; +import { useMembersAvatars } from '../../../hooks/useMembersAvatars'; import { Avatar, AvatarSize } from '../Avatar'; type Props = { diff --git a/ts/components/session/PillDivider.tsx b/ts/components/basic/PillDivider.tsx similarity index 100% rename from ts/components/session/PillDivider.tsx rename to ts/components/basic/PillDivider.tsx diff --git a/ts/components/session/SessionButton.tsx b/ts/components/basic/SessionButton.tsx similarity index 100% rename from ts/components/session/SessionButton.tsx rename to ts/components/basic/SessionButton.tsx diff --git a/ts/components/session/SessionDropdown.tsx b/ts/components/basic/SessionDropdown.tsx similarity index 96% rename from ts/components/session/SessionDropdown.tsx rename to ts/components/basic/SessionDropdown.tsx index 4c678251f..20c3348e1 100644 --- a/ts/components/session/SessionDropdown.tsx +++ b/ts/components/basic/SessionDropdown.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; +import { SessionIcon, SessionIconType } from '../icon'; -import { SessionIcon, SessionIconType } from './icon/'; import { SessionDropdownItem, SessionDropDownItemType } from './SessionDropdownItem'; // THIS IS DROPDOWN ACCORDIAN STYLE OPTIONS SELECTOR ELEMENT, NOT A CONTEXTMENU diff --git a/ts/components/session/SessionDropdownItem.tsx b/ts/components/basic/SessionDropdownItem.tsx similarity index 93% rename from ts/components/session/SessionDropdownItem.tsx rename to ts/components/basic/SessionDropdownItem.tsx index 7fccb5b98..3ef0ccc4f 100644 --- a/ts/components/session/SessionDropdownItem.tsx +++ b/ts/components/basic/SessionDropdownItem.tsx @@ -1,7 +1,6 @@ import React from 'react'; import classNames from 'classnames'; - -import { SessionIcon, SessionIconType } from './icon/'; +import { SessionIcon, SessionIconType } from '../icon'; export enum SessionDropDownItemType { Default = 'default', diff --git a/ts/components/session/SessionHTMLRenderer.tsx b/ts/components/basic/SessionHTMLRenderer.tsx similarity index 91% rename from ts/components/session/SessionHTMLRenderer.tsx rename to ts/components/basic/SessionHTMLRenderer.tsx index da0310f72..225a907f9 100644 --- a/ts/components/session/SessionHTMLRenderer.tsx +++ b/ts/components/basic/SessionHTMLRenderer.tsx @@ -20,6 +20,7 @@ export const SessionHtmlRenderer: React.SFC = ({ tag = 'div', key, html, return React.createElement(tag, { key, className, + // tslint:disable-next-line: react-no-dangerous-html dangerouslySetInnerHTML: { __html: clean }, }); }; diff --git a/ts/components/basic/SessionIdEditable.tsx b/ts/components/basic/SessionIdEditable.tsx new file mode 100644 index 000000000..b43d8d526 --- /dev/null +++ b/ts/components/basic/SessionIdEditable.tsx @@ -0,0 +1,54 @@ +import React, { ChangeEvent, KeyboardEvent, useRef } from 'react'; +import classNames from 'classnames'; +import { useFocusMount } from '../../hooks/useFocusMount'; + +type Props = { + placeholder?: string; + value?: string; + text?: string; + editable?: boolean; + onChange?: (value: string) => void; + onPressEnter?: any; + maxLength?: number; + isGroup?: boolean; +}; + +export const SessionIdEditable = (props: Props) => { + const { placeholder, onPressEnter, onChange, editable, text, value, maxLength, isGroup } = props; + const inputRef = useRef(null); + + useFocusMount(inputRef, editable); + function handleChange(e: ChangeEvent) { + if (editable && onChange) { + const eventValue = e.target.value?.replace(/(\r\n|\n|\r)/gm, ''); + onChange(eventValue); + } + } + + function handleKeyDown(e: KeyboardEvent) { + if (editable && e.key === 'Enter') { + e.preventDefault(); + // tslint:disable-next-line: no-unused-expression + onPressEnter && onPressEnter(); + } + } + + return ( +
+