diff --git a/BUILDING.md b/BUILDING.md index a3f13486f..6a8359d57 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -124,6 +124,15 @@ yarn start-prod # start the app on production mode (currently this is the only o ### Commands +The `rpm` package is required for running the build-release script. Run the appropriate command to install the `rpm` package: +```sh +sudo pacman -S rpm # Arch +``` +```sh +sudo apt install rpm # Ubuntu/Debian +``` + + Run the following to build the binaries for your specific system OS. ``` diff --git a/package.json b/package.json index de6eefdc9..20719eb86 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.5", + "version": "1.8.6", "license": "GPL-3.0", "author": { "name": "Oxen Labs", diff --git a/ts/components/conversation/H5AudioPlayer.tsx b/ts/components/conversation/H5AudioPlayer.tsx index ce3658ad0..ed4bfb6fb 100644 --- a/ts/components/conversation/H5AudioPlayer.tsx +++ b/ts/components/conversation/H5AudioPlayer.tsx @@ -18,9 +18,10 @@ export const AudioPlayerWithEncryptedFile = (props: { contentType: string; messageId: string; }) => { + const { messageId, contentType, src } = props; const dispatch = useDispatch(); const [playbackSpeed, setPlaybackSpeed] = useState(1.0); - const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType, false); + const { urlToLoad } = useEncryptedFileFetch(src, contentType, false); const player = useRef(null); const autoPlaySetting = useSelector(getAudioAutoplay); @@ -30,16 +31,19 @@ export const AudioPlayerWithEncryptedFile = (props: { useEffect(() => { // updates playback speed to value selected in context menu - if (player.current?.audio.current?.playbackRate) { + if ( + player.current?.audio.current && + player.current?.audio.current?.playbackRate !== playbackSpeed + ) { player.current.audio.current.playbackRate = playbackSpeed; } }, [playbackSpeed, player]); useEffect(() => { - if (props.messageId === nextMessageToPlayId) { + if (messageId !== undefined && messageId === nextMessageToPlayId) { player.current?.audio.current?.play(); } - }, [props.messageId, nextMessageToPlayId, player]); + }, [messageId, nextMessageToPlayId, player]); const triggerPlayNextMessageIfNeeded = (endedMessageId: string) => { const justEndedMessageIndex = messageProps.findIndex( @@ -75,8 +79,8 @@ export const AudioPlayerWithEncryptedFile = (props: { const onEnded = () => { // if audio autoplay is enabled, call method to start playing // the next playable message - if (autoPlaySetting === true && props.messageId) { - triggerPlayNextMessageIfNeeded(props.messageId); + if (autoPlaySetting === true && messageId) { + triggerPlayNextMessageIfNeeded(messageId); } }; @@ -87,6 +91,8 @@ export const AudioPlayerWithEncryptedFile = (props: { style={{ pointerEvents: multiSelectMode ? 'none' : 'inherit' }} layout="horizontal-reverse" showSkipControls={false} + autoPlay={false} + autoPlayAfterSrcChange={false} showJumpControls={false} showDownloadProgress={false} listenInterval={100} diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx index 9365a09ff..af0ce7584 100644 --- a/ts/components/conversation/message/message-item/ReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx @@ -144,9 +144,18 @@ export const ReadableMessage = (props: ReadableMessageProps) => { const found = await getMessageById(messageId); if (found && Boolean(found.get('unread'))) { + const foundReceivedAt = found.get('received_at'); // mark the message as read. // this will trigger the expire timer. await found.markRead(Date.now()); + + // we should stack those and send them in a single message once every 5secs or something. + // this would be part of an redesign of the sending pipeline + if (foundReceivedAt) { + void getConversationController() + .get(found.id) + ?.sendReadReceiptsIfNeeded([foundReceivedAt]); + } } } } diff --git a/ts/data/data.ts b/ts/data/data.ts index 2191d1279..5cfd0c232 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -90,6 +90,12 @@ function _cleanData(data: any): any { data[key] = value.map(_cleanData); } else if (_.isObject(value) && value instanceof File) { data[key] = { name: value.name, path: value.path, size: value.size, type: value.type }; + } else if (_.isObject(value) && value instanceof ArrayBuffer) { + window.log.error( + 'Trying to save an ArrayBuffer to the db is most likely an error. This specific field should be removed before the cleanData call' + ); + /// just skip it + continue; } else if (_.isObject(value)) { data[key] = _cleanData(value); } else if (_.isBoolean(value)) { diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 8ae1feebf..cfcab74c7 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -231,7 +231,10 @@ export class ConversationModel extends Backbone.Model { if (newestUnreadDate > lastReadTimestamp) { this.lastReadTimestamp = newestUnreadDate; } - void markReadDebounced(newestUnreadDate); + + if (newestUnreadDate !== lastReadTimestamp) { + void markReadDebounced(newestUnreadDate); + } }; // Listening for out-of-band data updates @@ -1059,6 +1062,7 @@ export class ConversationModel extends Backbone.Model { } } + // tslint:disable-next-line: cyclomatic-complexity public async markReadBouncy(newestUnreadDate: number, providedOptions: any = {}) { const lastReadTimestamp = this.lastReadTimestamp; if (newestUnreadDate < lastReadTimestamp) { @@ -1107,7 +1111,7 @@ export class ConversationModel extends Backbone.Model { const realUnreadCount = await this.getUnreadCount(); if (read.length === 0) { const cachedUnreadCountOnConvo = this.get('unreadCount'); - if (cachedUnreadCountOnConvo !== read.length) { + if (cachedUnreadCountOnConvo !== realUnreadCount) { // reset the unreadCount on the convo to the real one coming from markRead messages on the db this.set({ unreadCount: realUnreadCount }); await this.commit(); @@ -1142,25 +1146,31 @@ export class ConversationModel extends Backbone.Model { // conversation is viewed, another error message shows up for the contact read = read.filter(item => !item.hasErrors); - if (this.isPublic()) { + if (read.length && options.sendReadReceipts) { + const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array; + await this.sendReadReceiptsIfNeeded(timestamps); + } + } + + public async sendReadReceiptsIfNeeded(timestamps: Array) { + if (!this.isPrivate() || !timestamps.length) { return; } - if (this.isPrivate() && read.length && options.sendReadReceipts) { - window?.log?.info( - `Sending ${read.length} read receipts?`, - Storage.get(SettingsKey.settingsReadReceipt) || false - ); - const dontSendReceipt = this.isBlocked() || this.isIncomingRequest(); - if (Storage.get(SettingsKey.settingsReadReceipt) && !dontSendReceipt) { - const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array; - const receiptMessage = new ReadReceiptMessage({ - timestamp: Date.now(), - timestamps, - }); + const settingsReadReceiptEnabled = Storage.get(SettingsKey.settingsReadReceipt) || false; + const sendReceipt = + settingsReadReceiptEnabled && !this.isBlocked() && !this.isIncomingRequest(); - const device = new PubKey(this.id); - await getMessageQueue().sendToPubKey(device, receiptMessage); - } + if (sendReceipt) { + window?.log?.info(`Sending ${timestamps.length} read receipts.`); + // we should probably stack read receipts and send them every 5 seconds for instance per conversation + + const receiptMessage = new ReadReceiptMessage({ + timestamp: Date.now(), + timestamps, + }); + + const device = new PubKey(this.id); + await getMessageQueue().sendToPubKey(device, receiptMessage); } } diff --git a/ts/models/message.ts b/ts/models/message.ts index 043e68bca..d16e57701 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1048,7 +1048,8 @@ export class MessageModel extends Backbone.Model { } perfStart(`messageCommit-${this.attributes.id}`); - const id = await saveMessage(this.attributes); + // because the saving to db calls _cleanData which mutates the field for cleaning, we need to save a copy + const id = await saveMessage(_.cloneDeep(this.attributes)); if (triggerUIUpdate) { this.dispatchMessageUpdate(); } diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 8ebc37455..45bdbd5e4 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -447,11 +447,9 @@ export async function innerHandleSwarmContentMessage( } function onReadReceipt(readAt: number, timestamp: number, source: string) { - const { storage } = window; - window?.log?.info('read receipt', source, timestamp); - if (!storage.get(SettingsKey.settingsReadReceipt)) { + if (!Storage.get(SettingsKey.settingsReadReceipt)) { return; } diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index cea551632..ff459a935 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -201,7 +201,7 @@ export async function handleSwarmDataMessage( ); window?.log?.info( - `Handle dataMessage about convo ${convoIdToAddTheMessageTo} from user: ${convoIdOfSender}: ${cleanDataMessage}` + `Handle dataMessage about convo ${convoIdToAddTheMessageTo} from user: ${convoIdOfSender}` ); // remove the prefix from the source object so this is correct for all other diff --git a/ts/session/utils/AttachmentsDownload.ts b/ts/session/utils/AttachmentsDownload.ts index 8d2b78df7..940ba2bf9 100644 --- a/ts/session/utils/AttachmentsDownload.ts +++ b/ts/session/utils/AttachmentsDownload.ts @@ -75,7 +75,7 @@ export async function addJob(attachment: any, job: any = {}) { const toSave = { ...job, id, - attachment, + attachment: omit(attachment, ['toJSON']), // when addJob is called from the receiver we get an object with a toJSON call we don't care timestamp, pending: 0, attempts: 0, diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index cc6d81aad..b8a963d27 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -25,6 +25,11 @@ export const getSearchResults = createSelector( searchState.contactsAndGroups.map(id => { const value = lookup[id]; + // on some edges cases, we have an id but no corresponding convo because it matches a query but the conversation was removed. + if (!value) { + return null; + } + // 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