diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fe19c3d27..8d30bb871 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1590,6 +1590,10 @@ "message": "Friend request declined", "description": "Shown in the conversation history when the user declines a friend request" }, + "friendRequestExpired": { + "message": "Friend request expired", + "description": "Shown in the conversation history when the users friend request expires" + }, "friendRequestNotificationTitle": { "message": "Friend request", "description": "Shown in a notification title when receiving a friend request" diff --git a/js/models/conversations.js b/js/models/conversations.js index 45eec3785..6efffe316 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -81,6 +81,7 @@ verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, keyExchangeCompleted: false, blockInput: false, + unlockTimestamp: null, // Timestamp used for expiring friend requests. }; }, @@ -158,6 +159,7 @@ this.unset('lastMessageStatus'); this.updateTextInputState(); + this.setFriendRequestExpiryTimeout(); }, isMe() { @@ -214,6 +216,7 @@ await this.inProgressFetch; removeMessage(); }, + async onCalculatingPoW(pubKey, timestamp) { if (this.id !== pubKey) return; @@ -227,7 +230,6 @@ if (setToExpire) model.setToExpire(); return model; }, - format() { const { format } = PhoneNumber; const regionCode = storage.get('regionCode'); @@ -518,8 +520,65 @@ if (pending.length > 0) this.notifyFriendRequest(this.id, 'accepted') }, + async onFriendRequestTimeout() { + // Unset the timer + if (this.unlockTimer) + clearTimeout(this.unlockTimer); + + this.unlockTimer = null; + + // Set the unlock timestamp to null + if (this.get('unlockTimestamp')) { + this.set({ unlockTimestamp: null }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } + + // Change any pending outgoing friend requests to expired + const outgoing = await this.getPendingFriendRequests('outgoing'); + await Promise.all( + outgoing.map(async request => { + if (request.hasErrors()) return; + + request.set({ friendStatus: 'expired' }); + await window.Signal.Data.saveMessage(request.attributes, { + Message: Whisper.Message, + }); + this.trigger('updateMessage', request); + }) + ); + + // Update the UI + this.updateFriendRequestUI(); + }, async onFriendRequestSent() { - return this.updateFriendRequestUI(); + // Check if we need to set the friend request expiry + const unlockTimestamp = this.get('unlockTimestamp'); + const isFriend = await this.isFriend(); + if (!isFriend && !unlockTimestamp) { + // Expire the messages after 72 hours + const hourLockDuration = 72; + const ms = 60 * 60 * 1000 * hourLockDuration; + + this.set({ unlockTimestamp: Date.now() + ms }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + + this.setFriendRequestExpiryTimeout(); + } + + this.updateFriendRequestUI(); + }, + setFriendRequestExpiryTimeout() { + const unlockTimestamp = this.get('unlockTimestamp'); + if (unlockTimestamp && !this.unlockTimer) { + const delta = Math.max(unlockTimestamp - Date.now(), 0); + this.unlockTimer = setTimeout(() => { + this.onFriendRequestTimeout(); + }, delta); + } }, isUnverified() { if (this.isPrivate()) { diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 2687ebea2..39fbfab87 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -475,7 +475,7 @@ OutgoingMessage.prototype = { } if (this.fallBackEncryption && conversation) { - conversation.onFriendRequestSent(); + await conversation.onFriendRequestSent(); } }) .then(this.reloadDevicesAndSend(number, true)) diff --git a/ts/components/conversation/FriendRequest.tsx b/ts/components/conversation/FriendRequest.tsx index 71f27b251..8366b40ba 100644 --- a/ts/components/conversation/FriendRequest.tsx +++ b/ts/components/conversation/FriendRequest.tsx @@ -8,7 +8,7 @@ interface Props { text: string; direction: 'incoming' | 'outgoing'; status: string; - friendStatus: 'pending' | 'accepted' | 'declined'; + friendStatus: 'pending' | 'accepted' | 'declined' | 'expired'; i18n: Localizer; onAccept: () => void; onDecline: () => void; @@ -22,11 +22,13 @@ export class FriendRequest extends React.Component { switch (friendStatus) { case 'pending': - return `friendRequestPending`; + return 'friendRequestPending'; case 'accepted': - return `friendRequestAccepted`; + return 'friendRequestAccepted'; case 'declined': - return `friendRequestDeclined`; + return 'friendRequestDeclined'; + case 'expired': + return 'friendRequestExpired' default: throw new Error(`Invalid friend request status: ${friendStatus}`); } @@ -45,7 +47,6 @@ export class FriendRequest extends React.Component { - ); } @@ -137,7 +138,7 @@ export class FriendRequest extends React.Component { public render() { const { direction } = this.props; - + return (
{ 'module-message-friend-request__container', )} > -