diff --git a/package.json b/package.json index 5c1af4ae5..94548dcba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.8.3", + "version": "1.8.4", "license": "GPL-3.0", "author": { "name": "Oxen Labs", diff --git a/preload.js b/preload.js index bc7dfa706..817e9c627 100644 --- a/preload.js +++ b/preload.js @@ -26,7 +26,6 @@ window.getNodeVersion = () => configAny.node_version; window.sessionFeatureFlags = { useOnionRequests: true, - useCallMessage: true, }; window.versionInfo = { diff --git a/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx index 644647eef..f4baa8c11 100644 --- a/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx +++ b/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -97,10 +97,7 @@ export const AvatarPlaceHolder = (props: Props) => { ); } - let initials = getInitials(name)?.toLocaleUpperCase() || '0'; - if (name.indexOf(' ') === -1) { - initials = name.substring(0, 2).toLocaleUpperCase(); - } + const initials = getInitials(name); const fontSize = Math.floor(initials.length > 1 ? diameter * 0.4 : diameter * 0.5); diff --git a/ts/components/calling/DraggableCallContainer.tsx b/ts/components/calling/DraggableCallContainer.tsx index b4ed84637..3de4ed894 100644 --- a/ts/components/calling/DraggableCallContainer.tsx +++ b/ts/components/calling/DraggableCallContainer.tsx @@ -9,6 +9,8 @@ import { openConversationWithMessages } from '../../state/ducks/conversations'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { useVideoCallEventsListener } from '../../hooks/useVideoEventListener'; import { VideoLoadingSpinner } from './InConversationCallContainer'; +import { getSection } from '../../state/selectors/section'; +import { SectionType } from '../../state/ducks/section'; export const DraggableCallWindow = styled.div` position: absolute; @@ -57,6 +59,7 @@ export const DraggableCallContainer = () => { const ongoingCallProps = useSelector(getHasOngoingCallWith); const selectedConversationKey = useSelector(getSelectedConversationKey); const hasOngoingCall = useSelector(getHasOngoingCall); + const selectedSection = useSelector(getSection); // the draggable container has a width of 12vw, so we just set it's X to a bit more than this const [positionX, setPositionX] = useState(window.innerWidth - (window.innerWidth * 1) / 6); @@ -99,7 +102,12 @@ export const DraggableCallContainer = () => { } }; - if (!hasOngoingCall || !ongoingCallProps || ongoingCallPubkey === selectedConversationKey) { + if ( + !hasOngoingCall || + !ongoingCallProps || + (ongoingCallPubkey === selectedConversationKey && + selectedSection.focusedSection !== SectionType.Settings) + ) { return null; } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 67373404b..26a00c894 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -211,13 +211,7 @@ const CallButton = () => { const hasOngoingCall = useSelector(getHasOngoingCall); const canCall = !(hasIncomingCall || hasOngoingCall); - if ( - !isPrivate || - isMe || - !selectedConvoKey || - isBlocked || - !window.sessionFeatureFlags.useCallMessage - ) { + if (!isPrivate || isMe || !selectedConvoKey || isBlocked) { return null; } diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 42b31bc84..da6b67108 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -52,6 +52,7 @@ import { SessionToastContainer } from '../SessionToastContainer'; import { LeftPaneSectionContainer } from './LeftPaneSectionContainer'; import { getLatestDesktopReleaseFileToFsV2 } from '../../session/apis/file_server_api/FileServerApiV2'; import { ipcRenderer } from 'electron'; +import { UserUtils } from '../../session/utils'; const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); @@ -184,6 +185,9 @@ const setupTheme = () => { // Do this only if we created a new Session ID, or if we already received the initial configuration message const triggerSyncIfNeeded = async () => { + await getConversationController() + .get(UserUtils.getOurPubKeyStrFromCache()) + .setDidApproveMe(true, true); const didWeHandleAConfigurationMessageAlready = (await getItemById(hasSyncedInitialConfigurationItem))?.value || false; if (didWeHandleAConfigurationMessageAlready) { diff --git a/ts/components/settings/section/CategoryPrivacy.tsx b/ts/components/settings/section/CategoryPrivacy.tsx index 59288bada..a91e6a4a4 100644 --- a/ts/components/settings/section/CategoryPrivacy.tsx +++ b/ts/components/settings/section/CategoryPrivacy.tsx @@ -69,17 +69,16 @@ export const SettingsCategoryPrivacy = (props: { description={window.i18n('mediaPermissionsDescription')} active={Boolean(window.getSettingValue('media-permissions'))} /> - {window.sessionFeatureFlags.useCallMessage && ( - { - await toggleCallMediaPermissions(forceUpdate); - forceUpdate(); - }} - title={window.i18n('callMediaPermissionsTitle')} - description={window.i18n('callMediaPermissionsDescription')} - active={Boolean(window.getCallMediaPermissions())} - /> - )} + { + await toggleCallMediaPermissions(forceUpdate); + forceUpdate(); + }} + title={window.i18n('callMediaPermissionsTitle')} + description={window.i18n('callMediaPermissionsDescription')} + active={Boolean(window.getCallMediaPermissions())} + /> + { const old = Boolean(window.getSettingValue(SettingsKey.settingsReadReceipt)); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 12950db24..a67243fae 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -427,7 +427,7 @@ export async function innerHandleSwarmContentMessage( if (content.unsendMessage) { await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend); } - if (content.callMessage && window.sessionFeatureFlags?.useCallMessage) { + if (content.callMessage) { await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage); } if (content.messageRequestResponse) { diff --git a/ts/session/utils/TypedEmitter.ts b/ts/session/utils/TypedEmitter.ts deleted file mode 100644 index a834ff6c6..000000000 --- a/ts/session/utils/TypedEmitter.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Code from https://github.com/andywer/typed-emitter - -type Arguments = [T] extends [(...args: infer U) => any] ? U : [T] extends [void] ? [] : [T]; - -/** - * Type-safe event emitter. - * - * Use it like this: - * - * interface MyEvents { - * error: (error: Error) => void - * message: (from: string, content: string) => void - * } - * - * const myEmitter = new EventEmitter() as TypedEmitter - * - * myEmitter.on("message", (from, content) => { - * // ... - * }) - * - * myEmitter.emit("error", "x") // <- Will catch this type error - * - * or - * - * class MyEmitter extends EventEmitter implements TypedEventEmitter - */ -export interface TypedEventEmitter { - addListener(event: E, listener: Events[E]): this; - on(event: E, listener: Events[E]): this; - once(event: E, listener: Events[E]): this; - prependListener(event: E, listener: Events[E]): this; - prependOnceListener(event: E, listener: Events[E]): this; - - off(event: E, listener: Events[E]): this; - removeAllListeners(event?: E): this; - removeListener(event: E, listener: Events[E]): this; - - emit(event: E, ...args: Arguments): boolean; - eventNames(): Array; - listeners(event: E): Array; - listenerCount(event: E): number; - - getMaxListeners(): number; - setMaxListeners(maxListeners: number): this; -} diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index f127032f3..f8c4819f5 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -5,6 +5,7 @@ import { openConversationWithMessages } from '../../../state/ducks/conversations import { answerCall, callConnected, + callReconnecting, CallStatusEnum, endCall, incomingCall, @@ -32,7 +33,7 @@ import { approveConvoAndSendResponse } from '../../../interactions/conversationI export type InputItem = { deviceId: string; label: string }; -export const callTimeoutMs = 30000; +export const callTimeoutMs = 60000; /** * This uuid is set only once we accepted a call or started one. @@ -41,6 +42,8 @@ let currentCallUUID: string | undefined; let currentCallStartTimestamp: number | undefined; +let weAreCallerOnCurrentCall: boolean | undefined; + const rejectedCallUUIDS: Set = new Set(); export type CallManagerOptionsType = { @@ -498,6 +501,7 @@ export async function USER_callRecipient(recipient: string) { window.log.info('Sending preOffer message to ', ed25519Str(recipient)); const calledConvo = getConversationController().get(recipient); calledConvo.set('active_at', Date.now()); // addSingleOutgoingMessage does the commit for us on the convo + weAreCallerOnCurrentCall = true; await calledConvo?.addSingleOutgoingMessage({ sent_at: now, @@ -598,7 +602,7 @@ function handleConnectionStateChanged(pubkey: string) { window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState); if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { - closeVideoCall(); + window.inboxStore?.dispatch(callReconnecting({ pubkey })); } else if (peerConnection?.connectionState === 'connected') { const firstAudioInput = audioInputsList?.[0].deviceId || undefined; if (firstAudioInput) { @@ -619,6 +623,7 @@ function handleConnectionStateChanged(pubkey: string) { function closeVideoCall() { window.log.info('closingVideoCall '); currentCallStartTimestamp = undefined; + weAreCallerOnCurrentCall = undefined; if (peerConnection) { peerConnection.ontrack = null; peerConnection.onicecandidate = null; @@ -747,11 +752,18 @@ function createOrGetPeerConnection(withPubkey: string) { if (peerConnection && peerConnection?.iceConnectionState === 'disconnected') { //this will trigger a negotation event with iceRestart set to true in the createOffer options set - global.setTimeout(() => { + global.setTimeout(async () => { window.log.info('onconnectionstatechange disconnected: restartIce()'); - if (peerConnection?.iceConnectionState === 'disconnected') { + if ( + peerConnection?.iceConnectionState === 'disconnected' && + withPubkey?.length && + weAreCallerOnCurrentCall === true + ) { + // we are the caller and the connection got dropped out, we need to send a new offer with iceRestart set to true. + // the recipient will get that new offer and send us a response back if he still online (peerConnection as any).restartIce(); + await createOfferAndSendIt(withPubkey); } }, 2000); } diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index 9ec887fa4..c3e942bca 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -10,7 +10,6 @@ import * as AttachmentDownloads from './AttachmentsDownload'; import * as CallManager from './calling/CallManager'; export * from './Attachments'; -export * from './TypedEmitter'; export * from './JobQueue'; export { diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx index 140469640..8e664ea7d 100644 --- a/ts/state/ducks/call.tsx +++ b/ts/state/ducks/call.tsx @@ -76,6 +76,22 @@ const callSlice = createSlice({ state.callIsInFullScreen = false; return state; }, + callReconnecting(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + if (callerPubkey !== state.ongoingWith) { + window.log.info('cannot reconnect a call we did not start or receive first'); + return state; + } + const existingCallState = state.ongoingCallStatus; + + if (existingCallState !== 'ongoing') { + window.log.info('cannot reconnect a call we are not ongoing'); + return state; + } + + state.ongoingCallStatus = 'connecting'; + 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'); @@ -112,6 +128,7 @@ export const { endCall, answerCall, callConnected, + callReconnecting, startingCallWith, setFullScreenCall, } = actions; diff --git a/ts/test/session/unit/utils/Emoji_test.ts b/ts/test/session/unit/utils/Emoji_test.ts new file mode 100644 index 000000000..9391a08b5 --- /dev/null +++ b/ts/test/session/unit/utils/Emoji_test.ts @@ -0,0 +1,132 @@ +import { expect } from 'chai'; +import { getEmojiSizeClass } from '../../../../util/emoji'; + +describe('getEmojiSizeClass', () => { + describe('empty or null string', () => { + it('undefined as string', () => { + expect(getEmojiSizeClass(undefined as any)).to.be.equal('small', 'should have return small'); + }); + it('null as string', () => { + expect(getEmojiSizeClass(null as any)).to.be.equal('small', 'should have return small'); + }); + + it('empty string', () => { + expect(getEmojiSizeClass('')).to.be.equal('small', 'should have return small'); + }); + }); + + describe('with only characters not emojis of ascii/utf8', () => { + it('string of ascii only', () => { + expect( + getEmojiSizeClass('The ASCII compatible UTF-8 encoding of ISO 10646 and Unicode') + ).to.be.equal('small', 'should have return small'); + }); + + it('string of utf8 with weird chars but no', () => { + expect(getEmojiSizeClass('ASCII safety test: 1lI|, 0OD, 8B')).to.be.equal( + 'small', + 'should have return small' + ); + }); + + it('string of utf8 with weird chars', () => { + // taken from https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html + expect( + getEmojiSizeClass('ASCII safety test: 1lI|, 0OD, 8B, γιγνώσκειν, ὦ ἄνδρες დასასწრებად') + ).to.be.equal('small', 'should have return small'); + }); + + it('short string of utf8 with weird chars', () => { + // taken from https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html + expect(getEmojiSizeClass('დ')).to.be.equal('small', 'should have return small'); + }); + }); + + describe('with string containing utf8 emojis', () => { + describe('with string containing utf8 emojis and normal characters', () => { + it('one emoji after a normal sentence', () => { + expect( + getEmojiSizeClass('The SMILING FACE WITH HORNS character (😈) is assigned') + ).to.be.equal('small', 'should have return small'); + }); + + it('multiple emoji after a normal sentence', () => { + expect( + getEmojiSizeClass('The SMILING FACE WITH HORNS character (😈) is assigned 😈 😈') + ).to.be.equal('small', 'should have return small'); + }); + + it('multiple emoji before a normal sentence', () => { + expect( + getEmojiSizeClass('😈 😈The SMILING FACE WITH HORNS character () is assigned') + ).to.be.equal('small', 'should have return small'); + }); + + it('one emoji with just a space after', () => { + expect(getEmojiSizeClass('😈 ')).to.be.equal('jumbo', 'should have return jumbo'); + }); + + it('one emoji with just a space before', () => { + expect(getEmojiSizeClass(' 😈')).to.be.equal('jumbo', 'should have return jumbo'); + }); + + it('one emoji with just a space before & after', () => { + expect(getEmojiSizeClass(' 😈 ')).to.be.equal('jumbo', 'should have return jumbo'); + }); + }); + describe('with string containing only emojis ', () => { + it('one emoji without other characters', () => { + expect(getEmojiSizeClass('😈')).to.be.equal('jumbo', 'should have return jumbo'); + }); + + it('two emoji without other characters', () => { + expect(getEmojiSizeClass('😈😈')).to.be.equal('jumbo', 'should have return jumbo'); + }); + + it('3 emoji without other characters', () => { + expect(getEmojiSizeClass('😈😈😈')).to.be.equal('large', 'should have return large'); + }); + + it('4 emoji without other characters', () => { + expect(getEmojiSizeClass('😈😈😈😈')).to.be.equal('large', 'should have return large'); + }); + + it('5 emoji without other characters', () => { + expect(getEmojiSizeClass('😈😈😈😈😈')).to.be.equal('medium', 'should have return medium'); + }); + + it('6 emoji without other characters', () => { + expect(getEmojiSizeClass('😈😈😈😈😈😈')).to.be.equal( + 'medium', + 'should have return medium' + ); + }); + + it('7 emoji without other characters', () => { + expect(getEmojiSizeClass('😈😈😈😈😈😈😈')).to.be.equal( + 'small', + 'should have return small' + ); + }); + + it('lots of emojis without other characters', () => { + expect( + getEmojiSizeClass( + '😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈' + ) + ).to.be.equal('small', 'should have return small'); + }); + + it('lots of emojis without other characters except space', () => { + expect(getEmojiSizeClass('😈😈😈😈😈😈😈😈😈😈😈 😈😈 😈😈 😈😈 ')).to.be.equal( + 'small', + 'should have return small' + ); + }); + + it('3 emojis without other characters except space', () => { + expect(getEmojiSizeClass('😈 😈 😈 ')).to.be.equal('large', 'should have return small'); + }); + }); + }); +}); diff --git a/ts/test/session/unit/utils/Initials_test.ts b/ts/test/session/unit/utils/Initials_test.ts new file mode 100644 index 000000000..e86cb4376 --- /dev/null +++ b/ts/test/session/unit/utils/Initials_test.ts @@ -0,0 +1,82 @@ +import { expect } from 'chai'; +import { getInitials } from '../../../../util/getInitials'; + +describe('getInitials', () => { + describe('empty or null string', () => { + it('initials: return undefined if string is undefined', () => { + expect(getInitials(undefined)).to.be.equal('0', 'should have return 0'); + }); + + it('initials: return undefined if string is empty', () => { + expect(getInitials('')).to.be.equal('0', 'should have return 0'); + }); + + it('initials: return undefined if string is null', () => { + expect(getInitials(null as any)).to.be.equal('0', 'should have return 0'); + }); + }); + + describe('name is a pubkey', () => { + it('initials: return the first char after 05 if it starts with 05 and has length >2 ', () => { + expect(getInitials('052')).to.be.equal('2', 'should have return 2'); + }); + + it('initials: return the first char after 05 capitalized if it starts with 05 and has length >2 ', () => { + expect(getInitials('05bcd')).to.be.equal('B', 'should have return B'); + }); + + it('initials: return the first char after 05 if it starts with 05 and has length >2 ', () => { + expect(getInitials('059052052052052052052052')).to.be.equal('9', 'should have return 9'); + }); + }); + + describe('name has a space in its content', () => { + it('initials: return the first char of each first 2 words if a space is present ', () => { + expect(getInitials('John Doe')).to.be.equal('JD', 'should have return JD'); + }); + + it('initials: return the first char capitalized of each first 2 words if a space is present ', () => { + expect(getInitials('John doe')).to.be.equal('JD', 'should have return JD capitalized'); + }); + + it('initials: return the first char capitalized of each first 2 words if a space is present, even with more than 2 words ', () => { + expect(getInitials('John Doe Alice')).to.be.equal('JD', 'should have return JD capitalized'); + }); + + it('initials: return the first char capitalized of each first 2 words if a space is present, even with more than 2 words ', () => { + expect(getInitials('John doe Alice')).to.be.equal('JD', 'should have return JD capitalized'); + }); + + describe('name is not ascii', () => { + // ß maps to SS in uppercase + it('initials: shorten to 2 char at most if the uppercase form length is > 2 ', () => { + expect(getInitials('John ß')).to.be.equal('JS', 'should have return JS capitalized'); + }); + + it('initials: shorten to 2 char at most if the uppercase form length is > 2 ', () => { + expect(getInitials('ß ß')).to.be.equal('SS', 'should have return SS capitalized'); + }); + }); + }); + + describe('name has NO spaces in its content', () => { + it('initials: return the first 2 chars of the first word if the name has no space ', () => { + expect(getInitials('JOHNY')).to.be.equal('JO', 'should have return JO'); + }); + + it('initials: return the first 2 chars capitalized of the first word if the name has no space ', () => { + expect(getInitials('Johnny')).to.be.equal('JO', 'should have return JO'); + }); + + describe('name is not ascii', () => { + // ß maps to SS in uppercase + it('initials: shorten to 2 char at most if the uppercase form length is > 2 ', () => { + expect(getInitials('ß')).to.be.equal('SS', 'should have return SS capitalized'); + }); + + it('initials: shorten to 2 char at most if the uppercase form length is > 2 ', () => { + expect(getInitials('ßß')).to.be.equal('SS', 'should have return SS capitalized'); + }); + }); + }); +}); diff --git a/ts/test/session/unit/utils/TimerBucket_test.ts b/ts/test/session/unit/utils/TimerBucket_test.ts new file mode 100644 index 000000000..43e81fd52 --- /dev/null +++ b/ts/test/session/unit/utils/TimerBucket_test.ts @@ -0,0 +1,172 @@ +import { expect } from 'chai'; +import { getIncrement, getTimerBucketIcon } from '../../../../util/timer'; + +describe('getIncrement', () => { + describe('negative length', () => { + it('length < 0', () => { + expect(getIncrement(-1)).to.be.equal(1000, 'should have return 1000'); + }); + + it('length < -1000', () => { + expect(getIncrement(-1000)).to.be.equal(1000, 'should have return 1000'); + }); + }); + + describe('positive length but less than a minute => should return 500', () => { + it('length = 60000', () => { + expect(getIncrement(60000)).to.be.equal(500, 'should have return 500'); + }); + + it('length = 10000', () => { + expect(getIncrement(10000)).to.be.equal(500, 'should have return 500'); + }); + + it('length = 0', () => { + expect(getIncrement(0)).to.be.equal(500, 'should have return 500'); + }); + }); + + describe('positive length > a minute => should return Math.ceil(length / 12) ', () => { + it('length = 2 minutes', () => { + expect(getIncrement(120000)).to.be.equal(10000, 'should have return 10000'); + }); + + it('length = 2 minutes not divisible by 12', () => { + // because we have Math.ceil() + expect(getIncrement(120001)).to.be.equal(10001, 'should have return 10000'); + }); + + it('length = 20 days', () => { + expect(getIncrement(1000 * 60 * 60 * 24 * 20)).to.be.equal( + 144000000, + 'should have return 144000000' + ); + }); + + it('length = 20 days not divisible by 12', () => { + // because we have Math.ceil() + expect(getIncrement(1000 * 60 * 60 * 24 * 20 + 1)).to.be.equal( + 144000001, + 'should have return 144000001' + ); + }); + }); +}); + +describe('getTimerBucketIcon', () => { + describe('absolute values', () => { + it('delta < 0', () => { + expect(getTimerBucketIcon(Date.now() - 1000, 100)).to.be.equal( + 'timer60', + 'should have return timer60' + ); + }); + + it('delta > length by a little', () => { + expect(getTimerBucketIcon(Date.now() + 101, 100)).to.be.equal( + 'timer00', + 'should have return timer00' + ); + }); + + it('delta > length by a lot', () => { + expect(getTimerBucketIcon(Date.now() + 10100000, 100)).to.be.equal( + 'timer00', + 'should have return timer00' + ); + }); + }); + + describe('calculated values for length 1000', () => { + const length = 1000; + it('delta = 0', () => { + expect(getTimerBucketIcon(Date.now(), length)).to.be.equal( + 'timer00', + 'should have return timer00' + ); + }); + it('delta = 1/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (1 / 12) * length, length)).to.be.equal( + 'timer05', + 'should have return timer05' + ); + }); + + it('delta = 2/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (2 / 12) * length, length)).to.be.equal( + 'timer10', + 'should have return timer10' + ); + }); + + it('delta = 3/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (3 / 12) * length, length)).to.be.equal( + 'timer15', + 'should have return timer15' + ); + }); + + it('delta = 4/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (4 / 12) * length, length)).to.be.equal( + 'timer20', + 'should have return timer20' + ); + }); + + it('delta = 5/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (5 / 12) * length, length)).to.be.equal( + 'timer25', + 'should have return timer25' + ); + }); + + it('delta = 6/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (6 / 12) * length, length)).to.be.equal( + 'timer30', + 'should have return timer30' + ); + }); + + it('delta = 7/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (7 / 12) * length, length)).to.be.equal( + 'timer35', + 'should have return timer35' + ); + }); + + it('delta = 8/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (8 / 12) * length, length)).to.be.equal( + 'timer40', + 'should have return timer40' + ); + }); + + it('delta = 9/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (9 / 12) * length, length)).to.be.equal( + 'timer45', + 'should have return timer45' + ); + }); + + it('delta = 10/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (10 / 12) * length, length)).to.be.equal( + 'timer50', + 'should have return timer50' + ); + }); + + it('delta = 11/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (11 / 12) * length, length)).to.be.equal( + 'timer55', + 'should have return timer55' + ); + }); + + it('delta = 12/12 of length', () => { + expect(getTimerBucketIcon(Date.now() + (12 / 12) * length, length)).to.be.equal( + 'timer60', + 'should have return timer60' + ); + }); + }); +}); diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index a5690fd18..c662b86cb 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -179,6 +179,9 @@ async function registrationDone(ourPubkey: string, displayName: string) { ); await conversation.setLokiProfile({ displayName }); await conversation.setIsApproved(true); + await conversation.setDidApproveMe(true); + + await conversation.commit(); const user = { ourNumber: getOurPubKeyStrFromCache(), ourPrimary: ourPubkey, diff --git a/ts/util/emoji.ts b/ts/util/emoji.ts index 1f171a621..90357d5e4 100644 --- a/ts/util/emoji.ts +++ b/ts/util/emoji.ts @@ -18,14 +18,15 @@ function hasNormalCharacters(str: string) { } export function getEmojiSizeClass(str: string): SizeClassType { + if (!str || !str.length) { + return 'small'; + } if (hasNormalCharacters(str)) { return 'small'; } const emojiCount = getCountOfAllMatches(str); - if (emojiCount > 8) { - return 'small'; - } else if (emojiCount > 6) { + if (emojiCount > 6) { return 'small'; } else if (emojiCount > 4) { return 'medium'; diff --git a/ts/util/getInitials.ts b/ts/util/getInitials.ts index 8c7bb4777..c2ac667b8 100644 --- a/ts/util/getInitials.ts +++ b/ts/util/getInitials.ts @@ -1,18 +1,35 @@ -export function getInitials(name?: string): string | undefined { +export function getInitials(name?: string): string { if (!name || !name.length) { - return; + return '0'; } if (name.length > 2 && name.startsWith('05')) { - return name[2]; + // Just the third char of the pubkey when the name is a pubkey + return upperAndShorten(name[2]); } - const initials = name - .split(' ') - .slice(0, 2) - .map(n => { - return n[0]; - }); + if (name.indexOf(' ') === -1) { + // there is no space, just return the first 2 chars of the name - return initials.join(''); + if (name.length > 1) { + return upperAndShorten(name.slice(0, 2)); + } + return upperAndShorten(name[0]); + } + + // name has a space, just extract the first char of each words + return upperAndShorten( + name + .split(' ') + .slice(0, 2) + .map(n => { + return n[0]; + }) + .join('') + ); +} + +function upperAndShorten(str: string) { + // believe it or not, some chars put in uppercase can be more than one char. (ß for instance) + return str.toLocaleUpperCase().slice(0, 2); } diff --git a/ts/window.d.ts b/ts/window.d.ts index 89629304b..0b0c2b994 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -37,7 +37,6 @@ declare global { log: any; sessionFeatureFlags: { useOnionRequests: boolean; - useCallMessage: boolean; }; SessionSnodeAPI: SessionSnodeAPI; onLogin: any;