Message Requests
This commit is contained in:
parent
4d4b7a26a5
commit
83574eb067
60 changed files with 2566 additions and 216 deletions
|
@ -1583,6 +1583,10 @@
|
||||||
"message": "Note to Self",
|
"message": "Note to Self",
|
||||||
"description": "Name for the conversation with your own phone number"
|
"description": "Name for the conversation with your own phone number"
|
||||||
},
|
},
|
||||||
|
"noteToSelfHero": {
|
||||||
|
"message": "You can add notes for yourself in this conversation. If your account has any linked devices, new notes will be synced.",
|
||||||
|
"description": "Description for the Note to Self conversation"
|
||||||
|
},
|
||||||
"hideMenuBar": {
|
"hideMenuBar": {
|
||||||
"message": "Hide menu bar",
|
"message": "Hide menu bar",
|
||||||
"description": "Label text for menu bar visibility setting"
|
"description": "Label text for menu bar visibility setting"
|
||||||
|
@ -2315,5 +2319,219 @@
|
||||||
"ReactionsViewer--all": {
|
"ReactionsViewer--all": {
|
||||||
"message": "All",
|
"message": "All",
|
||||||
"description": "Shown in reaction viewer as the title for the 'all' category"
|
"description": "Shown in reaction viewer as the title for the 'all' category"
|
||||||
|
},
|
||||||
|
"MessageRequests--message-direct": {
|
||||||
|
"message": "Do you want to let $name$ message you? They won't know you've seen their message until you accept.",
|
||||||
|
"description": "Shown as the message for a message request in a direct message",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--message-direct-blocked": {
|
||||||
|
"message": "Unblock $name$ to message and call each other.",
|
||||||
|
"description": "Shown as the message for a message request in a direct message with a blocked account",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--message-group": {
|
||||||
|
"message": "Do you want to join $group$? They won't know you've seen their message until you accept.",
|
||||||
|
"description": "Shown as the message for a message request in a group",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--message-group-blocked": {
|
||||||
|
"message": "Unblock to allow group members to add you to this group again.",
|
||||||
|
"description": "Shown as the message for a message request in a blocked group"
|
||||||
|
},
|
||||||
|
"MessageRequests--block": {
|
||||||
|
"message": "Block",
|
||||||
|
"description": "Shown as a button to let the user block a message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--unblock": {
|
||||||
|
"message": "Unblock",
|
||||||
|
"description": "Shown as a button to let the user unblock a message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--unblock-confirm-title": {
|
||||||
|
"message": "Unblock $name$?",
|
||||||
|
"description": "Shown as a button to let the user unblock a message request",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--unblock-direct-confirm-body": {
|
||||||
|
"message": "You will be able to message and call each other.",
|
||||||
|
"description": "Shown as the body in the confirmation modal for unblocking a private message request",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--unblock-group-confirm-body": {
|
||||||
|
"message": "Group members will be able to add your to this group again.",
|
||||||
|
"description": "Shown as the body in the confirmation modal for unblocking a group message request",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--block-and-delete": {
|
||||||
|
"message": "Block and Delete",
|
||||||
|
"description": "Shown as a button to let the user block and delete a message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--block-direct-confirm-title": {
|
||||||
|
"message": "Block $name$?",
|
||||||
|
"description": "Shown as the title in the confirmation modal for blocking a private message request",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Cayce Pollard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--block-direct-confirm-body": {
|
||||||
|
"message": "Blocked people won't be able to call you or send you mesages.",
|
||||||
|
"description": "Shown as the body in the confirmation modal for blocking a private message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--block-group-confirm-title": {
|
||||||
|
"message": "Block and Leave $group$?",
|
||||||
|
"description": "Shown as the title in the confirmation modal for blocking a group message request",
|
||||||
|
"placeholders": {
|
||||||
|
"group": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Friends 🌿"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--block-group-confirm-body": {
|
||||||
|
"message": "You will no longer receive messages or updates from this group and members won't be able to add you to this group again.",
|
||||||
|
"description": "Shown as the body in the confirmation modal for blocking a group message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--delete": {
|
||||||
|
"message": "Delete",
|
||||||
|
"description": "Shown as a button to let the user delete any message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--delete-direct-confirm-title": {
|
||||||
|
"message": "Delete conversation?",
|
||||||
|
"description": "Shown as the title in the confirmation modal for deleting a private message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--delete-direct-confirm-body": {
|
||||||
|
"message": "This conversation will be deleted from all of your devices.",
|
||||||
|
"description": "Shown as the body in the confirmation modal for deleting a private message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--delete-group-confirm-title": {
|
||||||
|
"message": "Delete and Leave $group$?",
|
||||||
|
"description": "Shown as the title in the confirmation modal for deleting a group message request",
|
||||||
|
"placeholders": {
|
||||||
|
"group": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Friends 🌿"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MessageRequests--delete-direct": {
|
||||||
|
"message": "Delete",
|
||||||
|
"description": "Shown as a button to let the user delete a direct message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--delete-group": {
|
||||||
|
"message": "Delete and Leave",
|
||||||
|
"description": "Shown as a button to let the user delete a group message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--delete-group-confirm-body": {
|
||||||
|
"message": "You will leave this group, and it will be deleted from all your devices.",
|
||||||
|
"description": "Shown as the body in the confirmation modal for deleting a group message request"
|
||||||
|
},
|
||||||
|
"MessageRequests--accept": {
|
||||||
|
"message": "Accept",
|
||||||
|
"description": "Shown as a button to let the user accept a message request"
|
||||||
|
},
|
||||||
|
"ConversationHero--members": {
|
||||||
|
"message": "$count$ members",
|
||||||
|
"description": "Specifies the number of members in a group conversation",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "22"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConversationHero--members-1": {
|
||||||
|
"message": "1 member",
|
||||||
|
"description": "Specifies the number of members in a group conversation when there is one member",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "22"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConversationHero--membership-1": {
|
||||||
|
"message": "Member of $group$.",
|
||||||
|
"description": "Shown in the conversation hero to indicate this user is a member of a mutual group",
|
||||||
|
"placeholders": {
|
||||||
|
"group": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "NYC Rock Climbers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConversationHero--membership-2": {
|
||||||
|
"message": "Member of $group1$ and $group2$.",
|
||||||
|
"description": "Shown in the conversation hero to indicate this user is a member of at least two mutual groups",
|
||||||
|
"placeholders": {
|
||||||
|
"group1": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "NYC Rock Climbers"
|
||||||
|
},
|
||||||
|
"group2": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Dinner Party"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConversationHero--membership-3": {
|
||||||
|
"message": "Member of $group1$, $group2$, and $group3$.",
|
||||||
|
"description": "Shown in the conversation hero to indicate this user is a member of at least three mutual groups",
|
||||||
|
"placeholders": {
|
||||||
|
"group1": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "NYC Rock Climbers"
|
||||||
|
},
|
||||||
|
"group2": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Dinner Party"
|
||||||
|
},
|
||||||
|
"group3": {
|
||||||
|
"content": "$3",
|
||||||
|
"example": "Friends 🌿"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConversationHero--membership-added": {
|
||||||
|
"message": "$name$ added you to the group.",
|
||||||
|
"description": "Shown Indicates that you were added to a group by a given individual.",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Jeff Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,6 +355,7 @@
|
||||||
<script type='text/javascript' src='js/read_receipts.js'></script>
|
<script type='text/javascript' src='js/read_receipts.js'></script>
|
||||||
<script type='text/javascript' src='js/read_syncs.js'></script>
|
<script type='text/javascript' src='js/read_syncs.js'></script>
|
||||||
<script type='text/javascript' src='js/view_syncs.js'></script>
|
<script type='text/javascript' src='js/view_syncs.js'></script>
|
||||||
|
<script type='text/javascript' src='js/message_requests.js'></script>
|
||||||
<script type='text/javascript' src='js/reactions.js'></script>
|
<script type='text/javascript' src='js/reactions.js'></script>
|
||||||
<script type='text/javascript' src='js/deletes.js'></script>
|
<script type='text/javascript' src='js/deletes.js'></script>
|
||||||
<script type='text/javascript' src='js/libphonenumber-util.js'></script>
|
<script type='text/javascript' src='js/libphonenumber-util.js'></script>
|
||||||
|
|
BIN
fixtures/kitten-4-112-112.jpg
Normal file
BIN
fixtures/kitten-4-112-112.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -1479,6 +1479,34 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.Signal.RemoteConfig.initRemoteConfig();
|
||||||
|
|
||||||
|
// Maybe refresh remote configuration when we become active
|
||||||
|
window.registerForActive(async () => {
|
||||||
|
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for changes to the `desktop.messageRequests` remote configuration flag
|
||||||
|
const removeMessageRequestListener = window.Signal.RemoteConfig.onChange(
|
||||||
|
'desktop.messageRequests',
|
||||||
|
({ enabled }) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversations = window.getConversations();
|
||||||
|
conversations.forEach(conversation => {
|
||||||
|
conversation.set({
|
||||||
|
messageCountBeforeMessageRequests:
|
||||||
|
conversation.get('messageCount') || 0,
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeMessageRequestListener();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.getSyncRequest = () =>
|
window.getSyncRequest = () =>
|
||||||
|
@ -1629,6 +1657,8 @@
|
||||||
addQueuedEventListener('typing', onTyping);
|
addQueuedEventListener('typing', onTyping);
|
||||||
addQueuedEventListener('sticker-pack', onStickerPack);
|
addQueuedEventListener('sticker-pack', onStickerPack);
|
||||||
addQueuedEventListener('viewSync', onViewSync);
|
addQueuedEventListener('viewSync', onViewSync);
|
||||||
|
addQueuedEventListener('messageRequestResponse', onMessageRequestResponse);
|
||||||
|
addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate);
|
||||||
|
|
||||||
window.Signal.AttachmentDownloads.start({
|
window.Signal.AttachmentDownloads.start({
|
||||||
getMessageReceiver: () => messageReceiver,
|
getMessageReceiver: () => messageReceiver,
|
||||||
|
@ -2258,7 +2288,10 @@
|
||||||
|
|
||||||
const result = ConversationController.getOrCreate(
|
const result = ConversationController.getOrCreate(
|
||||||
messageDescriptor.id,
|
messageDescriptor.id,
|
||||||
messageDescriptor.type
|
messageDescriptor.type,
|
||||||
|
messageDescriptor.type === 'group'
|
||||||
|
? { addedBy: message.getContact().get('id') }
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
if (messageDescriptor.type === 'private') {
|
if (messageDescriptor.type === 'private') {
|
||||||
|
@ -2306,6 +2339,41 @@
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onProfileKeyUpdate({ data, confirm }) {
|
||||||
|
const conversation = ConversationController.get(
|
||||||
|
data.source || data.sourceUuid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.error(
|
||||||
|
'onProfileKeyUpdate: could not find conversation',
|
||||||
|
data.source,
|
||||||
|
data.sourceUuid
|
||||||
|
);
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.profileKey) {
|
||||||
|
window.log.error(
|
||||||
|
'onProfileKeyUpdate: missing profileKey',
|
||||||
|
data.profileKey
|
||||||
|
);
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
'onProfileKeyUpdate: updating profileKey',
|
||||||
|
data.source,
|
||||||
|
data.sourceUuid
|
||||||
|
);
|
||||||
|
|
||||||
|
await conversation.setProfileKey(data.profileKey);
|
||||||
|
|
||||||
|
confirm();
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMessageSentProfileUpdate({
|
async function handleMessageSentProfileUpdate({
|
||||||
data,
|
data,
|
||||||
confirm,
|
confirm,
|
||||||
|
@ -2318,7 +2386,7 @@
|
||||||
type
|
type
|
||||||
);
|
);
|
||||||
|
|
||||||
conversation.set({ profileSharing: true });
|
conversation.enableProfileSharing();
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
|
||||||
// Then we update our own profileKey if it's different from what we have
|
// Then we update our own profileKey if it's different from what we have
|
||||||
|
@ -2635,6 +2703,25 @@
|
||||||
Whisper.ViewSyncs.onSync(sync);
|
Whisper.ViewSyncs.onSync(sync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onMessageRequestResponse(ev) {
|
||||||
|
ev.confirm();
|
||||||
|
|
||||||
|
const { threadE164, threadUuid, groupId, messageRequestResponseType } = ev;
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
threadE164,
|
||||||
|
threadUuid,
|
||||||
|
groupId,
|
||||||
|
type: messageRequestResponseType,
|
||||||
|
};
|
||||||
|
|
||||||
|
window.log.info('message request response', args);
|
||||||
|
|
||||||
|
const sync = Whisper.MessageRequests.add(args);
|
||||||
|
|
||||||
|
Whisper.MessageRequests.onResponse(sync);
|
||||||
|
}
|
||||||
|
|
||||||
function onReadReceipt(ev) {
|
function onReadReceipt(ev) {
|
||||||
const readAt = ev.timestamp;
|
const readAt = ev.timestamp;
|
||||||
const { timestamp } = ev.read;
|
const { timestamp } = ev.read;
|
||||||
|
|
91
js/message_requests.js
Normal file
91
js/message_requests.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/* global
|
||||||
|
Backbone,
|
||||||
|
Whisper,
|
||||||
|
ConversationController,
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.Whisper = window.Whisper || {};
|
||||||
|
Whisper.MessageRequests = new (Backbone.Collection.extend({
|
||||||
|
forConversation(conversation) {
|
||||||
|
if (conversation.get('e164')) {
|
||||||
|
const syncByE164 = this.findWhere({
|
||||||
|
e164: conversation.get('e164'),
|
||||||
|
});
|
||||||
|
if (syncByE164) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for E164 ${conversation.get(
|
||||||
|
'e164'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.remove(syncByE164);
|
||||||
|
return syncByE164;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation.get('uuid')) {
|
||||||
|
const syncByUuid = this.findWhere({
|
||||||
|
uuid: conversation.get('uuid'),
|
||||||
|
});
|
||||||
|
if (syncByUuid) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for UUID ${conversation.get(
|
||||||
|
'uuid'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.remove(syncByUuid);
|
||||||
|
return syncByUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation.get('groupId')) {
|
||||||
|
const syncByGroupId = this.findWhere({
|
||||||
|
uuid: conversation.get('groupId'),
|
||||||
|
});
|
||||||
|
if (syncByGroupId) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for GROUP ID ${conversation.get(
|
||||||
|
'groupId'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.remove(syncByGroupId);
|
||||||
|
return syncByGroupId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
async onResponse(sync) {
|
||||||
|
try {
|
||||||
|
const threadE164 = sync.get('threadE164');
|
||||||
|
const threadUuid = sync.get('threadUuid');
|
||||||
|
const groupId = sync.get('groupId');
|
||||||
|
const identifier = threadE164 || threadUuid || groupId;
|
||||||
|
const conversation = ConversationController.get(identifier);
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
window.log(
|
||||||
|
`Received message request response for unknown conversation: ${identifier}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.applyMessageRequestResponse(sync.get('type'), {
|
||||||
|
fromSync: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.remove(sync);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'MessageRequests.onResponse error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))();
|
||||||
|
})();
|
|
@ -1,4 +1,4 @@
|
||||||
/* global storage, _ */
|
/* global storage, _, ConversationController */
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
(function() {
|
(function() {
|
||||||
|
@ -79,4 +79,48 @@
|
||||||
window.log.info(`removing group(${groupId} from blocked list`);
|
window.log.info(`removing group(${groupId} from blocked list`);
|
||||||
storage.put(BLOCKED_GROUPS_ID, _.without(groupIds, groupId));
|
storage.put(BLOCKED_GROUPS_ID, _.without(groupIds, groupId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimistically adds a conversation to our local block list.
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
storage.blockIdentifier = id => {
|
||||||
|
const conv = ConversationController.get(id);
|
||||||
|
if (conv) {
|
||||||
|
const uuid = conv.get('uuid');
|
||||||
|
if (uuid) {
|
||||||
|
storage.addBlockedUuid(uuid);
|
||||||
|
}
|
||||||
|
const e164 = conv.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
storage.addBlockedNumber(e164);
|
||||||
|
}
|
||||||
|
const groupId = conv.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
storage.addBlockedGroup(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimistically removes a conversation from our local block list.
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
storage.unblockIdentifier = id => {
|
||||||
|
const conv = ConversationController.get(id);
|
||||||
|
if (conv) {
|
||||||
|
const uuid = conv.get('uuid');
|
||||||
|
if (uuid) {
|
||||||
|
storage.removeBlockedUuid(uuid);
|
||||||
|
}
|
||||||
|
const e164 = conv.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
storage.removeBlockedNumber(e164);
|
||||||
|
}
|
||||||
|
const groupId = conv.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
storage.removeBlockedGroup(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
libsignal,
|
libsignal,
|
||||||
storage,
|
storage,
|
||||||
textsecure,
|
textsecure,
|
||||||
Whisper
|
Whisper,
|
||||||
|
Signal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
@ -60,6 +61,8 @@
|
||||||
return {
|
return {
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||||
|
messageCount: 0,
|
||||||
|
sentMessageCount: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -97,6 +100,8 @@
|
||||||
this.ourNumber = textsecure.storage.user.getNumber();
|
this.ourNumber = textsecure.storage.user.getNumber();
|
||||||
this.ourUuid = textsecure.storage.user.getUuid();
|
this.ourUuid = textsecure.storage.user.getUuid();
|
||||||
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
|
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
|
||||||
|
this.messageRequestEnum =
|
||||||
|
textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||||
|
|
||||||
// This may be overridden by ConversationController.getOrCreate, and signify
|
// This may be overridden by ConversationController.getOrCreate, and signify
|
||||||
// our first save to the database. Or first fetch from the database.
|
// our first save to the database. Or first fetch from the database.
|
||||||
|
@ -164,6 +169,52 @@
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isBlocked() {
|
||||||
|
const uuid = this.get('uuid');
|
||||||
|
if (uuid) {
|
||||||
|
return window.storage.isUuidBlocked(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const e164 = this.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
return window.storage.isBlocked(e164);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = this.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
return window.storage.isGroupBlocked(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
unblock() {
|
||||||
|
const uuid = this.get('uuid');
|
||||||
|
if (uuid) {
|
||||||
|
return window.storage.removeBlockedUuid(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const e164 = this.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
return window.storage.removeBlockedNumber(e164);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = this.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
return window.storage.removeBlockedGroup(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
enableProfileSharing() {
|
||||||
|
this.set({ profileSharing: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
disableProfileSharing() {
|
||||||
|
this.set({ profileSharing: false });
|
||||||
|
},
|
||||||
|
|
||||||
hasDraft() {
|
hasDraft() {
|
||||||
const draftAttachments = this.get('draftAttachments') || [];
|
const draftAttachments = this.get('draftAttachments') || [];
|
||||||
return (
|
return (
|
||||||
|
@ -320,6 +371,10 @@
|
||||||
this.messageCollection.remove(id);
|
this.messageCollection.remove(id);
|
||||||
existing.trigger('expired');
|
existing.trigger('expired');
|
||||||
existing.cleanup();
|
existing.cleanup();
|
||||||
|
|
||||||
|
// An expired message only counts as decrementing the message count, not
|
||||||
|
// the sent message count
|
||||||
|
this.decrementMessageCount();
|
||||||
};
|
};
|
||||||
|
|
||||||
// If a fetch is in progress, then we need to wait until that's complete to
|
// If a fetch is in progress, then we need to wait until that's complete to
|
||||||
|
@ -390,11 +445,15 @@
|
||||||
const shouldShowDraft =
|
const shouldShowDraft =
|
||||||
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
|
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
|
||||||
const inboxPosition = this.get('inbox_position');
|
const inboxPosition = this.get('inbox_position');
|
||||||
|
const messageRequestsEnabled = Signal.RemoteConfig.isEnabled(
|
||||||
|
'desktop.messageRequests'
|
||||||
|
);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
||||||
isArchived: this.get('isArchived'),
|
isArchived: this.get('isArchived'),
|
||||||
|
isBlocked: this.isBlocked(),
|
||||||
activeAt: this.get('active_at'),
|
activeAt: this.get('active_at'),
|
||||||
avatarPath: this.getAvatarPath(),
|
avatarPath: this.getAvatarPath(),
|
||||||
color,
|
color,
|
||||||
|
@ -414,11 +473,17 @@
|
||||||
draftText,
|
draftText,
|
||||||
|
|
||||||
phoneNumber: this.getNumber(),
|
phoneNumber: this.getNumber(),
|
||||||
|
membersCount: this.isPrivate()
|
||||||
|
? undefined
|
||||||
|
: (this.get('members') || []).length,
|
||||||
lastMessage: {
|
lastMessage: {
|
||||||
status: this.get('lastMessageStatus'),
|
status: this.get('lastMessageStatus'),
|
||||||
text: this.get('lastMessage'),
|
text: this.get('lastMessage'),
|
||||||
deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
|
deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptedMessageRequest: this.getAccepted(),
|
||||||
|
messageRequestsEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -449,6 +514,143 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
incrementMessageCount() {
|
||||||
|
this.set({
|
||||||
|
messageCount: (this.get('messageCount') || 0) + 1,
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
decrementMessageCount() {
|
||||||
|
this.set({
|
||||||
|
messageCount: Math.max((this.get('messageCount') || 0) - 1, 0),
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
incrementSentMessageCount() {
|
||||||
|
this.set({
|
||||||
|
messageCount: (this.get('messageCount') || 0) + 1,
|
||||||
|
sentMessageCount: (this.get('sentMessageCount') || 0) + 1,
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
decrementSentMessageCount() {
|
||||||
|
this.set({
|
||||||
|
messageCount: Math.max((this.get('messageCount') || 0) - 1, 0),
|
||||||
|
sentMessageCount: Math.max((this.get('sentMessageCount') || 0) - 1, 0),
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when a message request is accepted in order to
|
||||||
|
* handle sending read receipts and download any pending attachments.
|
||||||
|
*/
|
||||||
|
async handleReadAndDownloadAttachments() {
|
||||||
|
let messages;
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
messages = await window.Signal.Data.getOlderMessagesByConversation(
|
||||||
|
this.get('id'),
|
||||||
|
{
|
||||||
|
MessageCollection: Whisper.MessageCollection,
|
||||||
|
limit: 100,
|
||||||
|
receivedAt: messages
|
||||||
|
? messages.first().get('received_at')
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!messages.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readMessages = messages.filter(
|
||||||
|
m => !m.hasErrors() && m.isIncoming()
|
||||||
|
);
|
||||||
|
const receiptSpecs = readMessages.map(m => ({
|
||||||
|
sender: m.get('source') || m.get('sourceUuid'),
|
||||||
|
timestamp: m.get('sent_at'),
|
||||||
|
hasErrors: m.hasErrors(),
|
||||||
|
}));
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.sendReadReceiptsFor(receiptSpecs);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await Promise.all(readMessages.map(m => m.queueAttachmentDownloads()));
|
||||||
|
} while (messages.length > 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
async applyMessageRequestResponse(response, { fromSync = false } = {}) {
|
||||||
|
// Apply message request response locally
|
||||||
|
this.set({
|
||||||
|
messageRequestResponseType: response,
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
|
if (response === this.messageRequestEnum.ACCEPT) {
|
||||||
|
this.unblock();
|
||||||
|
this.enableProfileSharing();
|
||||||
|
this.sendProfileKeyUpdate();
|
||||||
|
if (!fromSync) {
|
||||||
|
// Locally accepted
|
||||||
|
await this.handleReadAndDownloadAttachments();
|
||||||
|
}
|
||||||
|
} else if (response === this.messageRequestEnum.BLOCK) {
|
||||||
|
// Block locally, other devices should block upon receiving the sync message
|
||||||
|
window.storage.blockIdentifier(this.get('id'));
|
||||||
|
this.disableProfileSharing();
|
||||||
|
} else if (response === this.messageRequestEnum.DELETE) {
|
||||||
|
// Delete messages locally, other devices should delete upon receiving
|
||||||
|
// the sync message
|
||||||
|
this.destroyMessages();
|
||||||
|
this.disableProfileSharing();
|
||||||
|
this.updateLastMessage();
|
||||||
|
if (!fromSync) {
|
||||||
|
this.trigger('unload', 'deleted from message request');
|
||||||
|
}
|
||||||
|
} else if (response === this.messageRequestEnum.BLOCK_AND_DELETE) {
|
||||||
|
// Delete messages locally, other devices should delete upon receiving
|
||||||
|
// the sync message
|
||||||
|
this.destroyMessages();
|
||||||
|
this.disableProfileSharing();
|
||||||
|
this.updateLastMessage();
|
||||||
|
// Block locally, other devices should block upon receiving the sync message
|
||||||
|
window.storage.blockIdentifier(this.get('id'));
|
||||||
|
// Leave group if this was a local action
|
||||||
|
if (!fromSync) {
|
||||||
|
this.leaveGroup();
|
||||||
|
this.trigger('unload', 'blocked and deleted from message request');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async syncMessageRequestResponse(response) {
|
||||||
|
// Let this run, no await
|
||||||
|
this.applyMessageRequestResponse(response);
|
||||||
|
|
||||||
|
const { ourNumber, ourUuid } = this;
|
||||||
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
ourNumber || ourUuid,
|
||||||
|
{
|
||||||
|
syncMessage: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await wrap(
|
||||||
|
textsecure.messaging.syncMessageRequestResponse(
|
||||||
|
{
|
||||||
|
threadE164: this.get('e164'),
|
||||||
|
threadUuid: this.get('uuid'),
|
||||||
|
groupId: this.get('groupId'),
|
||||||
|
type: response,
|
||||||
|
},
|
||||||
|
sendOptions
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
onMessageError() {
|
onMessageError() {
|
||||||
this.updateVerified();
|
this.updateVerified();
|
||||||
},
|
},
|
||||||
|
@ -687,6 +889,52 @@
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getSentMessageCount() {
|
||||||
|
return this.get('sentMessageCount') || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMessageRequestResponseType() {
|
||||||
|
return this.get('messageRequestResponseType') || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this conversation should be considered "accepted" in terms
|
||||||
|
* of message requests
|
||||||
|
*/
|
||||||
|
getAccepted() {
|
||||||
|
const messageRequestsEnabled = Signal.RemoteConfig.isEnabled(
|
||||||
|
'desktop.messageRequests'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!messageRequestsEnabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMe()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.getMessageRequestResponseType() === this.messageRequestEnum.ACCEPT
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromContact = this.getIsAddedByContact();
|
||||||
|
const hasSentMessages = this.getSentMessageCount() > 0;
|
||||||
|
const hasMessagesBeforeMessageRequests =
|
||||||
|
(this.get('messageCountBeforeMessageRequests') || 0) > 0;
|
||||||
|
const hasNoMessages = (this.get('messageCount') || 0) === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
fromContact ||
|
||||||
|
hasSentMessages ||
|
||||||
|
hasMessagesBeforeMessageRequests ||
|
||||||
|
hasNoMessages
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
onMemberVerifiedChange() {
|
onMemberVerifiedChange() {
|
||||||
// If the verified state of a member changes, our aggregate state changes.
|
// If the verified state of a member changes, our aggregate state changes.
|
||||||
// We trigger both events to replicate the behavior of Backbone.Model.set()
|
// We trigger both events to replicate the behavior of Backbone.Model.set()
|
||||||
|
@ -1159,6 +1407,33 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async sendProfileKeyUpdate() {
|
||||||
|
const id = this.get('id');
|
||||||
|
const recipients = this.isPrivate()
|
||||||
|
? [this.get('uuid') || this.get('e164')]
|
||||||
|
: this.getRecipients();
|
||||||
|
if (!this.get('profileSharing')) {
|
||||||
|
window.log.error(
|
||||||
|
'Attempted to send profileKeyUpdate to conversation without profileSharing enabled',
|
||||||
|
id,
|
||||||
|
recipients
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.log.info(
|
||||||
|
'Sending profileKeyUpdate to conversation',
|
||||||
|
id,
|
||||||
|
recipients
|
||||||
|
);
|
||||||
|
const profileKey = storage.get('profileKey');
|
||||||
|
await textsecure.messaging.sendProfileKeyUpdate(
|
||||||
|
profileKey,
|
||||||
|
recipients,
|
||||||
|
this.getSendOptions(),
|
||||||
|
this.get('groupId')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
sendMessage(body, attachments, quote, preview, sticker) {
|
sendMessage(body, attachments, quote, preview, sticker) {
|
||||||
this.clearTypingTimers();
|
this.clearTypingTimers();
|
||||||
|
|
||||||
|
@ -1226,6 +1501,7 @@
|
||||||
draft: null,
|
draft: null,
|
||||||
draftTimestamp: null,
|
draftTimestamp: null,
|
||||||
});
|
});
|
||||||
|
this.incrementSentMessageCount();
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
// We're offline!
|
// We're offline!
|
||||||
|
@ -1487,6 +1763,32 @@
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getIsContact() {
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
return Boolean(this.get('name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
getIsAddedByContact() {
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
return this.getIsContact();
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedBy = this.get('addedBy');
|
||||||
|
if (!addedBy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conv = ConversationController.get(addedBy);
|
||||||
|
if (!conv) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conv.getIsContact();
|
||||||
|
},
|
||||||
|
|
||||||
async updateLastMessage() {
|
async updateLastMessage() {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
return;
|
return;
|
||||||
|
@ -1793,7 +2095,7 @@
|
||||||
async leaveGroup() {
|
async leaveGroup() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (this.get('type') === 'group') {
|
if (this.get('type') === 'group') {
|
||||||
const groupNumbers = this.getRecipients();
|
const groupIdentifiers = this.getRecipients();
|
||||||
this.set({ left: true });
|
this.set({ left: true });
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
|
@ -1816,7 +2118,7 @@
|
||||||
const options = this.getSendOptions();
|
const options = this.getSendOptions();
|
||||||
message.send(
|
message.send(
|
||||||
this.wrapSend(
|
this.wrapSend(
|
||||||
textsecure.messaging.leaveGroup(this.id, groupNumbers, options)
|
textsecure.messaging.leaveGroup(this.id, groupIdentifiers, options)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1845,11 +2147,10 @@
|
||||||
// Note that this will update the message in the database
|
// Note that this will update the message in the database
|
||||||
await m.markRead(options.readAt);
|
await m.markRead(options.readAt);
|
||||||
|
|
||||||
const errors = m.get('errors');
|
|
||||||
return {
|
return {
|
||||||
sender: m.get('source'),
|
sender: m.get('source') || m.get('sourceUuid'),
|
||||||
timestamp: m.get('sent_at'),
|
timestamp: m.get('sent_at'),
|
||||||
hasErrors: Boolean(errors && errors.length),
|
hasErrors: m.hasErrors(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1870,7 +2171,7 @@
|
||||||
read = read.filter(item => !item.hasErrors);
|
read = read.filter(item => !item.hasErrors);
|
||||||
|
|
||||||
if (read.length && options.sendReadReceipts) {
|
if (read.length && options.sendReadReceipts) {
|
||||||
window.log.info(`Sending ${read.length} read receipts`);
|
window.log.info(`Sending ${read.length} read syncs`);
|
||||||
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||||
// to a contact, we need accessKeys for both.
|
// to a contact, we need accessKeys for both.
|
||||||
const { sendOptions } = ConversationController.prepareForSend(
|
const { sendOptions } = ConversationController.prepareForSend(
|
||||||
|
@ -1880,25 +2181,31 @@
|
||||||
await this.wrapSend(
|
await this.wrapSend(
|
||||||
textsecure.messaging.syncReadMessages(read, sendOptions)
|
textsecure.messaging.syncReadMessages(read, sendOptions)
|
||||||
);
|
);
|
||||||
|
await this.sendReadReceiptsFor(read);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
if (storage.get('read-receipt-setting')) {
|
async sendReadReceiptsFor(items) {
|
||||||
const convoSendOptions = this.getSendOptions();
|
// Only send read receipts for accepted conversations
|
||||||
|
if (storage.get('read-receipt-setting') && this.getAccepted()) {
|
||||||
|
window.log.info(`Sending ${items.length} read receipts`);
|
||||||
|
const convoSendOptions = this.getSendOptions();
|
||||||
|
const receiptsBySender = _.groupBy(items, 'sender');
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(_.groupBy(read, 'sender'), async (receipts, identifier) => {
|
_.map(receiptsBySender, async (receipts, identifier) => {
|
||||||
const timestamps = _.map(receipts, 'timestamp');
|
const timestamps = _.map(receipts, 'timestamp');
|
||||||
const c = ConversationController.get(identifier);
|
const c = ConversationController.get(identifier);
|
||||||
await this.wrapSend(
|
await this.wrapSend(
|
||||||
textsecure.messaging.sendReadReceipts(
|
textsecure.messaging.sendReadReceipts(
|
||||||
c.get('e164'),
|
c.get('e164'),
|
||||||
c.get('uuid'),
|
c.get('uuid'),
|
||||||
timestamps,
|
timestamps,
|
||||||
convoSendOptions
|
convoSendOptions
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -543,6 +543,9 @@
|
||||||
|
|
||||||
const conversation = this.getConversation();
|
const conversation = this.getConversation();
|
||||||
const isGroup = conversation && !conversation.isPrivate();
|
const isGroup = conversation && !conversation.isPrivate();
|
||||||
|
const conversationAccepted = Boolean(
|
||||||
|
conversation && conversation.getAccepted()
|
||||||
|
);
|
||||||
const sticker = this.get('sticker');
|
const sticker = this.get('sticker');
|
||||||
|
|
||||||
const isTapToView = this.isTapToView();
|
const isTapToView = this.isTapToView();
|
||||||
|
@ -577,6 +580,7 @@
|
||||||
textPending: this.get('bodyPending'),
|
textPending: this.get('bodyPending'),
|
||||||
id: this.id,
|
id: this.id,
|
||||||
conversationId: this.get('conversationId'),
|
conversationId: this.get('conversationId'),
|
||||||
|
conversationAccepted,
|
||||||
isSticker: Boolean(sticker),
|
isSticker: Boolean(sticker),
|
||||||
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
||||||
timestamp: this.get('sent_at'),
|
timestamp: this.get('sent_at'),
|
||||||
|
@ -1145,6 +1149,28 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isEmpty() {
|
||||||
|
const body = this.get('body');
|
||||||
|
const hasAttachment = (this.get('attachments') || []).length > 0;
|
||||||
|
const quote = this.get('quote');
|
||||||
|
const hasContact = (this.get('contact') || []).length > 0;
|
||||||
|
const sticker = this.get('sticker');
|
||||||
|
const hasPreview = (this.get('preview') || []).length > 0;
|
||||||
|
const groupUpdate = this.get('group_update');
|
||||||
|
const expirationTimerUpdate = this.get('expirationTimerUpdate');
|
||||||
|
|
||||||
|
const notEmpty = Boolean(
|
||||||
|
body ||
|
||||||
|
hasAttachment ||
|
||||||
|
quote ||
|
||||||
|
hasContact ||
|
||||||
|
sticker ||
|
||||||
|
hasPreview ||
|
||||||
|
groupUpdate ||
|
||||||
|
expirationTimerUpdate
|
||||||
|
);
|
||||||
|
return !notEmpty;
|
||||||
|
},
|
||||||
unload() {
|
unload() {
|
||||||
if (this.quotedMessage) {
|
if (this.quotedMessage) {
|
||||||
this.quotedMessage = null;
|
this.quotedMessage = null;
|
||||||
|
@ -1454,26 +1480,32 @@
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
canReply() {
|
canReply() {
|
||||||
|
const isAccepted = this.getConversation().getAccepted();
|
||||||
const errors = this.get('errors');
|
const errors = this.get('errors');
|
||||||
const isOutgoing = this.get('type') === 'outgoing';
|
const isOutgoing = this.get('type') === 'outgoing';
|
||||||
const numDelivered = this.get('delivered');
|
const numDelivered = this.get('delivered');
|
||||||
|
|
||||||
// Case 1: We cannot reply if this message is deleted for everyone
|
// Case 1: We cannot reply if we have accepted the message request
|
||||||
|
if (!isAccepted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: We cannot reply if this message is deleted for everyone
|
||||||
if (this.get('deletedForEveryone')) {
|
if (this.get('deletedForEveryone')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: We can reply if this is outgoing and delievered to at least one recipient
|
// Case 3: We can reply if this is outgoing and delievered to at least one recipient
|
||||||
if (isOutgoing && numDelivered > 0) {
|
if (isOutgoing && numDelivered > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 3: We can reply if there are no errors
|
// Case 4: We can reply if there are no errors
|
||||||
if (!errors || (errors && errors.length === 0)) {
|
if (!errors || (errors && errors.length === 0)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we cannot reply
|
// Case 5: default
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2171,10 +2203,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send delivery receipts, but only for incoming sealed sender messages
|
// Send delivery receipts, but only for incoming sealed sender messages
|
||||||
|
// and not for messages from unaccepted conversations
|
||||||
if (
|
if (
|
||||||
type === 'incoming' &&
|
type === 'incoming' &&
|
||||||
this.get('unidentifiedDeliveryReceived') &&
|
this.get('unidentifiedDeliveryReceived') &&
|
||||||
!this.hasErrors()
|
!this.hasErrors() &&
|
||||||
|
conversation.getAccepted()
|
||||||
) {
|
) {
|
||||||
// Note: We both queue and batch because we want to wait until we are done
|
// Note: We both queue and batch because we want to wait until we are done
|
||||||
// processing incoming messages to start sending outgoing delivery receipts.
|
// processing incoming messages to start sending outgoing delivery receipts.
|
||||||
|
@ -2344,6 +2378,7 @@
|
||||||
if (conversation.get('left')) {
|
if (conversation.get('left')) {
|
||||||
window.log.warn('re-added to a left group');
|
window.log.warn('re-added to a left group');
|
||||||
attributes.left = false;
|
attributes.left = false;
|
||||||
|
conversation.set({ addedBy: message.getContact().get('id') });
|
||||||
}
|
}
|
||||||
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
||||||
const sender = ConversationController.get(source || sourceUuid);
|
const sender = ConversationController.get(source || sourceUuid);
|
||||||
|
@ -2549,6 +2584,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop empty messages. This needs to happen after the initial
|
||||||
|
// message.set call to make sure all possible properties are set
|
||||||
|
// before we determine that a message is empty.
|
||||||
|
if (message.isEmpty()) {
|
||||||
|
window.log.info(
|
||||||
|
`Dropping empty datamessage ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const conversationTimestamp = conversation.get('timestamp');
|
const conversationTimestamp = conversation.get('timestamp');
|
||||||
if (
|
if (
|
||||||
!conversationTimestamp ||
|
!conversationTimestamp ||
|
||||||
|
@ -2561,9 +2607,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageController.register(message.id, message);
|
MessageController.register(message.id, message);
|
||||||
|
conversation.incrementMessageCount();
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
|
||||||
await message.queueAttachmentDownloads();
|
// Only queue attachments for downloads if this is an outgoing message
|
||||||
|
// or we've accepted the conversation
|
||||||
|
if (this.getConversation().getAccepted() || message.isOutgoing()) {
|
||||||
|
await message.queueAttachmentDownloads();
|
||||||
|
}
|
||||||
|
|
||||||
// Does this message have any pending, previously-received associated reactions?
|
// Does this message have any pending, previously-received associated reactions?
|
||||||
const reactions = Whisper.Reactions.forMessage(message);
|
const reactions = Whisper.Reactions.forMessage(message);
|
||||||
|
@ -2589,6 +2640,11 @@
|
||||||
await conversation.notify(message);
|
await conversation.notify(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Increment the sent message count if this is an outgoing message
|
||||||
|
if (type === 'outgoing') {
|
||||||
|
conversation.incrementSentMessageCount();
|
||||||
|
}
|
||||||
|
|
||||||
Whisper.events.trigger('incrementProgress');
|
Whisper.events.trigger('incrementProgress');
|
||||||
confirm();
|
confirm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ const Notifications = require('../../ts/notifications');
|
||||||
const OS = require('../../ts/OS');
|
const OS = require('../../ts/OS');
|
||||||
const Stickers = require('./stickers');
|
const Stickers = require('./stickers');
|
||||||
const Settings = require('./settings');
|
const Settings = require('./settings');
|
||||||
|
const RemoteConfig = require('../../ts/RemoteConfig');
|
||||||
const Util = require('../../ts/util');
|
const Util = require('../../ts/util');
|
||||||
const Metadata = require('./metadata/SecretSessionCipher');
|
const Metadata = require('./metadata/SecretSessionCipher');
|
||||||
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
||||||
|
@ -350,6 +351,7 @@ exports.setup = (options = {}) => {
|
||||||
Notifications,
|
Notifications,
|
||||||
OS,
|
OS,
|
||||||
RefreshSenderCertificate,
|
RefreshSenderCertificate,
|
||||||
|
RemoteConfig,
|
||||||
Settings,
|
Settings,
|
||||||
Services,
|
Services,
|
||||||
State,
|
State,
|
||||||
|
|
|
@ -367,6 +367,7 @@
|
||||||
color: this.model.getColor(),
|
color: this.model.getColor(),
|
||||||
avatarPath: this.model.getAvatarPath(),
|
avatarPath: this.model.getAvatarPath(),
|
||||||
|
|
||||||
|
isAccepted: this.model.getAccepted(),
|
||||||
isVerified: this.model.isVerified(),
|
isVerified: this.model.isVerified(),
|
||||||
isMe: this.model.isMe(),
|
isMe: this.model.isMe(),
|
||||||
isGroup: !this.model.isPrivate(),
|
isGroup: !this.model.isPrivate(),
|
||||||
|
@ -447,6 +448,9 @@
|
||||||
</div>
|
</div>
|
||||||
`)[0];
|
`)[0];
|
||||||
|
|
||||||
|
const messageRequestEnum =
|
||||||
|
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
id: this.model.id,
|
id: this.model.id,
|
||||||
compositionApi,
|
compositionApi,
|
||||||
|
@ -462,6 +466,26 @@
|
||||||
clearQuotedMessage: () => this.setQuoteMessage(null),
|
clearQuotedMessage: () => this.setQuoteMessage(null),
|
||||||
micCellEl,
|
micCellEl,
|
||||||
attachmentListEl,
|
attachmentListEl,
|
||||||
|
onAccept: this.model.syncMessageRequestResponse.bind(
|
||||||
|
this.model,
|
||||||
|
messageRequestEnum.ACCEPT
|
||||||
|
),
|
||||||
|
onBlock: this.model.syncMessageRequestResponse.bind(
|
||||||
|
this.model,
|
||||||
|
messageRequestEnum.BLOCK
|
||||||
|
),
|
||||||
|
onUnblock: this.model.syncMessageRequestResponse.bind(
|
||||||
|
this.model,
|
||||||
|
messageRequestEnum.ACCEPT
|
||||||
|
),
|
||||||
|
onDelete: this.model.syncMessageRequestResponse.bind(
|
||||||
|
this.model,
|
||||||
|
messageRequestEnum.DELETE
|
||||||
|
),
|
||||||
|
onBlockAndDelete: this.model.syncMessageRequestResponse.bind(
|
||||||
|
this.model,
|
||||||
|
messageRequestEnum.BLOCK_AND_DELETE
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||||
|
@ -1223,7 +1247,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._.pick(attachment, ['contentType', 'fileName', 'size', 'caption']),
|
..._.pick(attachment, [
|
||||||
|
'contentType',
|
||||||
|
'fileName',
|
||||||
|
'size',
|
||||||
|
'caption',
|
||||||
|
'blurHash',
|
||||||
|
]),
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -1433,6 +1463,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleImageAttachment(file) {
|
async handleImageAttachment(file) {
|
||||||
|
const blurHash = await window.imageToBlurHash(file);
|
||||||
if (MIME.isJPEG(file.type)) {
|
if (MIME.isJPEG(file.type)) {
|
||||||
const rotatedDataUrl = await window.autoOrientImage(file);
|
const rotatedDataUrl = await window.autoOrientImage(file);
|
||||||
const rotatedBlob = window.dataURLToBlobSync(rotatedDataUrl);
|
const rotatedBlob = window.dataURLToBlobSync(rotatedDataUrl);
|
||||||
|
@ -1454,6 +1485,7 @@
|
||||||
contentType,
|
contentType,
|
||||||
data,
|
data,
|
||||||
size: data.byteLength,
|
size: data.byteLength,
|
||||||
|
blurHash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1470,6 +1502,7 @@
|
||||||
contentType,
|
contentType,
|
||||||
data,
|
data,
|
||||||
size: data.byteLength,
|
size: data.byteLength,
|
||||||
|
blurHash,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2168,6 +2201,11 @@
|
||||||
});
|
});
|
||||||
message.trigger('unload');
|
message.trigger('unload');
|
||||||
this.model.messageCollection.remove(message.id);
|
this.model.messageCollection.remove(message.id);
|
||||||
|
if (message.isOutgoing()) {
|
||||||
|
this.model.decrementSentMessageCount();
|
||||||
|
} else {
|
||||||
|
this.model.decrementMessageCount();
|
||||||
|
}
|
||||||
this.resetPanel();
|
this.resetPanel();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
<script type="text/javascript" src="../../js/components.js" data-cover></script>
|
<script type="text/javascript" src="../../js/components.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/storage.js" data-cover></script>
|
<script type="text/javascript" src="../../js/storage.js" data-cover></script>
|
||||||
|
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
|
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
|
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/conversation_controller.js" data-cover></script>
|
<script type="text/javascript" src="../../js/conversation_controller.js" data-cover></script>
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"blob-util": "1.3.0",
|
"blob-util": "1.3.0",
|
||||||
"blueimp-canvas-to-blob": "3.14.0",
|
"blueimp-canvas-to-blob": "3.14.0",
|
||||||
"blueimp-load-image": "2.18.0",
|
"blueimp-load-image": "2.18.0",
|
||||||
|
"blurhash": "1.1.3",
|
||||||
"bunyan": "1.8.12",
|
"bunyan": "1.8.12",
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
"config": "1.28.1",
|
"config": "1.28.1",
|
||||||
|
@ -111,6 +112,7 @@
|
||||||
"proxy-agent": "3.1.1",
|
"proxy-agent": "3.1.1",
|
||||||
"qs": "6.5.1",
|
"qs": "6.5.1",
|
||||||
"react": "16.8.3",
|
"react": "16.8.3",
|
||||||
|
"react-blurhash": "0.1.2",
|
||||||
"react-contextmenu": "2.11.0",
|
"react-contextmenu": "2.11.0",
|
||||||
"react-dom": "16.8.3",
|
"react-dom": "16.8.3",
|
||||||
"react-dropzone": "10.1.7",
|
"react-dropzone": "10.1.7",
|
||||||
|
@ -157,6 +159,7 @@
|
||||||
"@storybook/react": "5.1.11",
|
"@storybook/react": "5.1.11",
|
||||||
"@types/agent-base": "4.2.0",
|
"@types/agent-base": "4.2.0",
|
||||||
"@types/backbone": "1.4.3",
|
"@types/backbone": "1.4.3",
|
||||||
|
"@types/blueimp-load-image": "2.23.6",
|
||||||
"@types/chai": "4.1.2",
|
"@types/chai": "4.1.2",
|
||||||
"@types/classnames": "2.2.3",
|
"@types/classnames": "2.2.3",
|
||||||
"@types/config": "0.0.34",
|
"@types/config": "0.0.34",
|
||||||
|
|
45
patches/react-blurhash+0.1.2.patch
Normal file
45
patches/react-blurhash+0.1.2.patch
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
diff --git a/node_modules/react-blurhash/lib/Blurhash.js b/node_modules/react-blurhash/lib/Blurhash.js
|
||||||
|
index db3115d..746c877 100644
|
||||||
|
--- a/node_modules/react-blurhash/lib/Blurhash.js
|
||||||
|
+++ b/node_modules/react-blurhash/lib/Blurhash.js
|
||||||
|
@@ -61,8 +61,8 @@ var Blurhash = /** @class */ (function (_super) {
|
||||||
|
};
|
||||||
|
Blurhash.prototype.render = function () {
|
||||||
|
var _a = this.props, hash = _a.hash, height = _a.height, width = _a.width, punch = _a.punch, resolutionX = _a.resolutionX, resolutionY = _a.resolutionY, style = _a.style, rest = __rest(_a, ["hash", "height", "width", "punch", "resolutionX", "resolutionY", "style"]);
|
||||||
|
- return (react_1.default.createElement("div", __assign({}, rest, { style: __assign({ display: 'inline-block', height: height, width: width }, style, { position: 'relative' }) }),
|
||||||
|
- react_1.default.createElement(BlurhashCanvas_1.default, { hash: hash, height: resolutionY, width: resolutionX, punch: punch, style: canvasStyle })));
|
||||||
|
+ return (react_1.createElement("div", __assign({}, rest, { style: __assign({ display: 'inline-block', height: height, width: width }, style, { position: 'relative' }) }),
|
||||||
|
+ react_1.createElement(BlurhashCanvas_1.default, { hash: hash, height: resolutionY, width: resolutionX, punch: punch, style: canvasStyle })));
|
||||||
|
};
|
||||||
|
Blurhash.defaultProps = {
|
||||||
|
height: 128,
|
||||||
|
@@ -71,6 +71,6 @@ var Blurhash = /** @class */ (function (_super) {
|
||||||
|
resolutionY: 32,
|
||||||
|
};
|
||||||
|
return Blurhash;
|
||||||
|
-}(react_1.default.PureComponent));
|
||||||
|
+}(react_1.PureComponent));
|
||||||
|
exports.default = Blurhash;
|
||||||
|
//# sourceMappingURL=Blurhash.js.map
|
||||||
|
\ No newline at end of file
|
||||||
|
diff --git a/node_modules/react-blurhash/lib/BlurhashCanvas.js b/node_modules/react-blurhash/lib/BlurhashCanvas.js
|
||||||
|
index b2833dc..d666520 100644
|
||||||
|
--- a/node_modules/react-blurhash/lib/BlurhashCanvas.js
|
||||||
|
+++ b/node_modules/react-blurhash/lib/BlurhashCanvas.js
|
||||||
|
@@ -63,13 +63,13 @@ var BlurhashCanvas = /** @class */ (function (_super) {
|
||||||
|
};
|
||||||
|
BlurhashCanvas.prototype.render = function () {
|
||||||
|
var _a = this.props, hash = _a.hash, height = _a.height, width = _a.width, rest = __rest(_a, ["hash", "height", "width"]);
|
||||||
|
- return react_1.default.createElement("canvas", __assign({}, rest, { height: height, width: width, ref: this.handleRef }));
|
||||||
|
+ return react_1.createElement("canvas", __assign({}, rest, { height: height, width: width, ref: this.handleRef }));
|
||||||
|
};
|
||||||
|
BlurhashCanvas.defaultProps = {
|
||||||
|
height: 128,
|
||||||
|
width: 128,
|
||||||
|
};
|
||||||
|
return BlurhashCanvas;
|
||||||
|
-}(react_1.default.PureComponent));
|
||||||
|
+}(react_1.PureComponent));
|
||||||
|
exports.default = BlurhashCanvas;
|
||||||
|
//# sourceMappingURL=BlurhashCanvas.js.map
|
||||||
|
\ No newline at end of file
|
|
@ -243,9 +243,11 @@ try {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const { autoOrientImage } = require('./js/modules/auto_orient_image');
|
const { autoOrientImage } = require('./js/modules/auto_orient_image');
|
||||||
|
const { imageToBlurHash } = require('./ts/util/imageToBlurHash');
|
||||||
|
|
||||||
window.autoOrientImage = autoOrientImage;
|
window.autoOrientImage = autoOrientImage;
|
||||||
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||||
|
window.imageToBlurHash = imageToBlurHash;
|
||||||
window.emojiData = require('emoji-datasource');
|
window.emojiData = require('emoji-datasource');
|
||||||
window.filesize = require('filesize');
|
window.filesize = require('filesize');
|
||||||
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||||
|
|
|
@ -325,17 +325,33 @@ message SyncMessage {
|
||||||
optional uint64 timestamp = 2;
|
optional uint64 timestamp = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Sent sent = 1;
|
message MessageRequestResponse {
|
||||||
optional Contacts contacts = 2;
|
enum Type {
|
||||||
optional Groups groups = 3;
|
UNKNOWN = 0;
|
||||||
optional Request request = 4;
|
ACCEPT = 1;
|
||||||
repeated Read read = 5;
|
DELETE = 2;
|
||||||
optional Blocked blocked = 6;
|
BLOCK = 3;
|
||||||
optional Verified verified = 7;
|
BLOCK_AND_DELETE = 4;
|
||||||
optional Configuration configuration = 9;
|
}
|
||||||
optional bytes padding = 8;
|
|
||||||
repeated StickerPackOperation stickerPackOperation = 10;
|
optional string threadE164 = 1;
|
||||||
optional ViewOnceOpen viewOnceOpen = 11;
|
optional string threadUuid = 2;
|
||||||
|
optional bytes groupId = 3;
|
||||||
|
optional Type type = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Sent sent = 1;
|
||||||
|
optional Contacts contacts = 2;
|
||||||
|
optional Groups groups = 3;
|
||||||
|
optional Request request = 4;
|
||||||
|
repeated Read read = 5;
|
||||||
|
optional Blocked blocked = 6;
|
||||||
|
optional Verified verified = 7;
|
||||||
|
optional Configuration configuration = 9;
|
||||||
|
optional bytes padding = 8;
|
||||||
|
repeated StickerPackOperation stickerPackOperation = 10;
|
||||||
|
optional ViewOnceOpen viewOnceOpen = 11;
|
||||||
|
optional MessageRequestResponse messageRequestResponse = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AttachmentPointer {
|
message AttachmentPointer {
|
||||||
|
|
|
@ -619,7 +619,7 @@
|
||||||
left: -12px;
|
left: -12px;
|
||||||
right: -12px;
|
right: -12px;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
bottom: -10px;
|
bottom: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
@ -643,6 +643,10 @@
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
border-top-right-radius: 0px;
|
border-top-right-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--with-collapsed-metadata {
|
||||||
|
margin-bottom: -10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__sticker-container {
|
.module-message__sticker-container {
|
||||||
|
@ -708,6 +712,11 @@
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__generic-attachment--not-active {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__generic-attachment__icon-container {
|
.module-message__generic-attachment__icon-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -3217,6 +3226,138 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
color: $color-gray-45;
|
color: $color-gray-45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: Conversation Hero
|
||||||
|
|
||||||
|
.module-conversation-hero {
|
||||||
|
padding: 32px 0 28px 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile-name {
|
||||||
|
@include font-title-2;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__with {
|
||||||
|
@include font-body-2;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__membership {
|
||||||
|
@include font-body-2;
|
||||||
|
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
@include font-body-2-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module: Message Request Actions
|
||||||
|
|
||||||
|
.module-message-request-actions {
|
||||||
|
padding: 8px 16px 12px 16px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background: $color-gray-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
@include font-body-2;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
@include font-body-2-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 80px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--deny {
|
||||||
|
color: $color-accent-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--accept {
|
||||||
|
color: $color-accent-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Conversation List Item
|
// Module: Conversation List Item
|
||||||
|
|
||||||
.module-conversation-list-item {
|
.module-conversation-list-item {
|
||||||
|
@ -3696,6 +3837,31 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
width: 62px;
|
width: 62px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-avatar--112 {
|
||||||
|
height: 112px;
|
||||||
|
width: 112px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 112px;
|
||||||
|
width: 112px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar__label--112 {
|
||||||
|
width: 112px;
|
||||||
|
font-size: 56px;
|
||||||
|
line-height: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-avatar__icon--112 {
|
||||||
|
height: 81px;
|
||||||
|
width: 81px;
|
||||||
|
}
|
||||||
|
.module-avatar__icon--112.module-avatar__icon--direct {
|
||||||
|
height: 87px;
|
||||||
|
width: 87px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-avatar__icon--note-to-self {
|
.module-avatar__icon--note-to-self {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
height: 70%;
|
height: 70%;
|
||||||
|
@ -3900,6 +4066,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-image--with-background {
|
.module-image--with-background {
|
||||||
|
@ -6471,8 +6638,13 @@ button.module-image__border-overlay:focus {
|
||||||
color: $color-gray-05;
|
color: $color-gray-05;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
margin-bottom: 20px;
|
@include font-body-1;
|
||||||
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
|
|
|
@ -545,6 +545,8 @@ describe('Backup', () => {
|
||||||
timestamp: 1524185933350,
|
timestamp: 1524185933350,
|
||||||
type: 'private',
|
type: 'private',
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
|
messageCount: 0,
|
||||||
|
sentMessageCount: 0,
|
||||||
verified: 0,
|
verified: 0,
|
||||||
sealedSender: 0,
|
sealedSender: 0,
|
||||||
version: 2,
|
version: 2,
|
||||||
|
|
94
ts/RemoteConfig.ts
Normal file
94
ts/RemoteConfig.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// tslint:disable: no-backbone-get-set-outside-model
|
||||||
|
import { get, throttle } from 'lodash';
|
||||||
|
import { WebAPIType } from './textsecure/WebAPI';
|
||||||
|
|
||||||
|
type ConfigKeyType = 'desktop.messageRequests';
|
||||||
|
type ConfigValueType = {
|
||||||
|
name: ConfigKeyType;
|
||||||
|
enabled: boolean;
|
||||||
|
enabledAt?: number;
|
||||||
|
};
|
||||||
|
type ConfigMapType = { [key: string]: ConfigValueType };
|
||||||
|
type ConfigListenerType = (value: ConfigValueType) => unknown;
|
||||||
|
type ConfigListenersMapType = {
|
||||||
|
[key: string]: Array<ConfigListenerType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getServer(): WebAPIType {
|
||||||
|
const OLD_USERNAME = window.storage.get<string>('number_id');
|
||||||
|
const USERNAME = window.storage.get<string>('uuid_id');
|
||||||
|
const PASSWORD = window.storage.get<string>('password');
|
||||||
|
|
||||||
|
return window.WebAPI.connect({
|
||||||
|
username: (USERNAME || OLD_USERNAME) as string,
|
||||||
|
password: PASSWORD as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ConfigMapType = {};
|
||||||
|
const listeners: ConfigListenersMapType = {};
|
||||||
|
|
||||||
|
export async function initRemoteConfig() {
|
||||||
|
config = window.storage.get('remoteConfig') || {};
|
||||||
|
await maybeRefreshRemoteConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onChange(key: ConfigKeyType, fn: ConfigListenerType) {
|
||||||
|
const keyListeners: Array<ConfigListenerType> = get(listeners, key, []);
|
||||||
|
keyListeners.push(fn);
|
||||||
|
listeners[key] = keyListeners;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
listeners[key] = listeners[key].filter(l => l !== fn);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshRemoteConfig = async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const server = getServer();
|
||||||
|
const newConfig = await server.getConfig();
|
||||||
|
|
||||||
|
// Process new configuration in light of the old configuration
|
||||||
|
// The old configuration is not set as the initial value in reduce because
|
||||||
|
// flags may have been deleted
|
||||||
|
const oldConfig = config;
|
||||||
|
config = newConfig.reduce((previous, { name, enabled }) => {
|
||||||
|
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
||||||
|
// If a flag was previously not enabled and is now enabled, record the time it was enabled
|
||||||
|
const enabledAt: number | undefined =
|
||||||
|
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
name: name as ConfigKeyType,
|
||||||
|
enabled,
|
||||||
|
enabledAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If enablement changes at all, notify listeners
|
||||||
|
const currentListeners = listeners[name] || [];
|
||||||
|
if (previouslyEnabled !== enabled) {
|
||||||
|
currentListeners.forEach(listener => {
|
||||||
|
listener(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return new configuration object
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
[name]: value,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
window.storage.put('remoteConfig', config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const maybeRefreshRemoteConfig = throttle(
|
||||||
|
refreshRemoteConfig,
|
||||||
|
// Only fetch remote configuration if the last fetch was more than two hours ago
|
||||||
|
2 * 60 * 60 * 1000,
|
||||||
|
{ trailing: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isEnabled(name: ConfigKeyType): boolean {
|
||||||
|
return get(config, [name, 'enabled'], false);
|
||||||
|
}
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="blue"
|
||||||
|
avatarPath={util.gifObjectUrl}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={80}
|
size={80}
|
||||||
color="blue"
|
color="blue"
|
||||||
|
@ -90,6 +97,13 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="blue"
|
||||||
|
name="One"
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={80}
|
size={80}
|
||||||
color="blue"
|
color="blue"
|
||||||
|
@ -143,6 +157,14 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="pink"
|
||||||
|
noteToSelf={true}
|
||||||
|
phoneNumber="(555) 353-3433"
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={80}
|
size={80}
|
||||||
color="pink"
|
color="pink"
|
||||||
|
@ -174,6 +196,7 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<Avatar size={112} color="blue" conversationType="group" i18n={util.i18n} />
|
||||||
<Avatar size={80} color="blue" conversationType="group" i18n={util.i18n} />
|
<Avatar size={80} color="blue" conversationType="group" i18n={util.i18n} />
|
||||||
<Avatar size={52} color="blue" conversationType="group" i18n={util.i18n} />
|
<Avatar size={52} color="blue" conversationType="group" i18n={util.i18n} />
|
||||||
<Avatar size={28} color="blue" conversationType="group" i18n={util.i18n} />
|
<Avatar size={28} color="blue" conversationType="group" i18n={util.i18n} />
|
||||||
|
@ -184,6 +207,7 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<Avatar size={112} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||||
<Avatar size={80} color="blue" conversationType="direct" i18n={util.i18n} />
|
<Avatar size={80} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||||
<Avatar size={52} color="blue" conversationType="direct" i18n={util.i18n} />
|
<Avatar size={52} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||||
<Avatar size={28} color="blue" conversationType="direct" i18n={util.i18n} />
|
<Avatar size={28} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||||
|
@ -361,6 +385,43 @@
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 112px
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="teal"
|
||||||
|
name="John Smith"
|
||||||
|
avatarPath={util.gifObjectUrl}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="teal"
|
||||||
|
name="John"
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="teal"
|
||||||
|
name="John Smith"
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<Avatar size={112} color="teal" conversationType="direct" i18n={util.i18n} />
|
||||||
|
<Avatar
|
||||||
|
size={112}
|
||||||
|
color="teal"
|
||||||
|
name="Pupplies"
|
||||||
|
conversationType="group"
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
### Broken color
|
### Broken color
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { getInitials } from '../util/getInitials';
|
import { getInitials } from '../util/getInitials';
|
||||||
import { ColorType, LocalizerType } from '../types/Util';
|
import { ColorType, LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export interface Props {
|
export type Props = {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color?: ColorType;
|
color?: ColorType;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ export interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
size: 28 | 32 | 52 | 80;
|
size: 28 | 32 | 52 | 80 | 112;
|
||||||
|
|
||||||
onClick?: () => unknown;
|
onClick?: () => unknown;
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export interface Props {
|
||||||
innerRef?: React.Ref<HTMLDivElement>;
|
innerRef?: React.Ref<HTMLDivElement>;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
} & Pick<React.HTMLProps<HTMLDivElement>, 'className'>;
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
imageBroken: boolean;
|
imageBroken: boolean;
|
||||||
|
@ -139,12 +139,13 @@ export class Avatar extends React.Component<Props, State> {
|
||||||
noteToSelf,
|
noteToSelf,
|
||||||
onClick,
|
onClick,
|
||||||
size,
|
size,
|
||||||
|
className,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
|
|
||||||
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
||||||
|
|
||||||
if (![28, 32, 52, 80].includes(size)) {
|
if (![28, 32, 52, 80, 112].includes(size)) {
|
||||||
throw new Error(`Size ${size} is not supported!`);
|
throw new Error(`Size ${size} is not supported!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +167,8 @@ export class Avatar extends React.Component<Props, State> {
|
||||||
'module-avatar',
|
'module-avatar',
|
||||||
`module-avatar--${size}`,
|
`module-avatar--${size}`,
|
||||||
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
|
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
|
||||||
!hasImage ? `module-avatar--${color}` : null
|
!hasImage ? `module-avatar--${color}` : null,
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
>
|
>
|
||||||
|
|
|
@ -16,11 +16,17 @@ import {
|
||||||
InputApi,
|
InputApi,
|
||||||
Props as CompositionInputProps,
|
Props as CompositionInputProps,
|
||||||
} from './CompositionInput';
|
} from './CompositionInput';
|
||||||
|
import {
|
||||||
|
MessageRequestActions,
|
||||||
|
Props as MessageRequestActionsProps,
|
||||||
|
} from './conversation/MessageRequestActions';
|
||||||
import { countStickers } from './stickers/lib';
|
import { countStickers } from './stickers/lib';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
|
readonly messageRequestsEnabled?: boolean;
|
||||||
|
readonly acceptedMessageRequest?: boolean;
|
||||||
readonly compositionApi?: React.MutableRefObject<{
|
readonly compositionApi?: React.MutableRefObject<{
|
||||||
focusInput: () => void;
|
focusInput: () => void;
|
||||||
isDirty: () => boolean;
|
isDirty: () => boolean;
|
||||||
|
@ -66,6 +72,7 @@ export type Props = Pick<
|
||||||
| 'showPickerHint'
|
| 'showPickerHint'
|
||||||
| 'clearShowPickerHint'
|
| 'clearShowPickerHint'
|
||||||
> &
|
> &
|
||||||
|
MessageRequestActionsProps &
|
||||||
OwnProps;
|
OwnProps;
|
||||||
|
|
||||||
const emptyElement = (el: HTMLElement) => {
|
const emptyElement = (el: HTMLElement) => {
|
||||||
|
@ -73,7 +80,7 @@ const emptyElement = (el: HTMLElement) => {
|
||||||
el.innerHTML = '';
|
el.innerHTML = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
||||||
export const CompositionArea = ({
|
export const CompositionArea = ({
|
||||||
i18n,
|
i18n,
|
||||||
attachmentListEl,
|
attachmentListEl,
|
||||||
|
@ -86,6 +93,8 @@ export const CompositionArea = ({
|
||||||
onEditorStateChange,
|
onEditorStateChange,
|
||||||
onTextTooLong,
|
onTextTooLong,
|
||||||
startingText,
|
startingText,
|
||||||
|
clearQuotedMessage,
|
||||||
|
getQuotedMessage,
|
||||||
// EmojiButton
|
// EmojiButton
|
||||||
onPickEmoji,
|
onPickEmoji,
|
||||||
onSetSkinTone,
|
onSetSkinTone,
|
||||||
|
@ -104,8 +113,19 @@ export const CompositionArea = ({
|
||||||
clearShowIntroduction,
|
clearShowIntroduction,
|
||||||
showPickerHint,
|
showPickerHint,
|
||||||
clearShowPickerHint,
|
clearShowPickerHint,
|
||||||
clearQuotedMessage,
|
// Message Requests
|
||||||
getQuotedMessage,
|
messageRequestsEnabled,
|
||||||
|
acceptedMessageRequest,
|
||||||
|
conversationType,
|
||||||
|
isBlocked,
|
||||||
|
name,
|
||||||
|
onAccept,
|
||||||
|
onBlock,
|
||||||
|
onBlockAndDelete,
|
||||||
|
onUnblock,
|
||||||
|
onDelete,
|
||||||
|
profileName,
|
||||||
|
phoneNumber,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [disabled, setDisabled] = React.useState(false);
|
const [disabled, setDisabled] = React.useState(false);
|
||||||
const [showMic, setShowMic] = React.useState(!startingText);
|
const [showMic, setShowMic] = React.useState(!startingText);
|
||||||
|
@ -299,6 +319,24 @@ export const CompositionArea = ({
|
||||||
};
|
};
|
||||||
}, [setLarge]);
|
}, [setLarge]);
|
||||||
|
|
||||||
|
if ((!acceptedMessageRequest || isBlocked) && messageRequestsEnabled) {
|
||||||
|
return (
|
||||||
|
<MessageRequestActions
|
||||||
|
i18n={i18n}
|
||||||
|
conversationType={conversationType}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
onBlock={onBlock}
|
||||||
|
onBlockAndDelete={onBlockAndDelete}
|
||||||
|
onUnblock={onUnblock}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onAccept={onAccept}
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-composition-area">
|
<div className="module-composition-area">
|
||||||
<div className="module-composition-area__toggle-large">
|
<div className="module-composition-area__toggle-large">
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
#### All Options
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<util.ConversationContext theme={util.theme}>
|
|
||||||
<ConfirmationDialog
|
|
||||||
i18n={util.i18n}
|
|
||||||
onClose={() => console.log('onClose')}
|
|
||||||
onAffirmative={() => console.log('onAffirmative')}
|
|
||||||
affirmativeText="Affirm"
|
|
||||||
onNegative={() => console.log('onNegative')}
|
|
||||||
negativeText="Negate"
|
|
||||||
>
|
|
||||||
asdf child
|
|
||||||
</ConfirmationDialog>
|
|
||||||
</util.ConversationContext>
|
|
||||||
```
|
|
40
ts/components/ConfirmationDialog.stories.tsx
Normal file
40
ts/components/ConfirmationDialog.stories.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
storiesOf('Components/ConfirmationDialog', module).add(
|
||||||
|
'ConfirmationDialog',
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<ConfirmationDialog
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={action('onClose')}
|
||||||
|
title={text('Title', 'Foo bar banana baz?')}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: 'Negate',
|
||||||
|
style: 'negative',
|
||||||
|
action: action('negative'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Affirm',
|
||||||
|
style: 'affirmative',
|
||||||
|
action: action('affirmative'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{text('Child text', 'asdf blip')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -2,14 +2,18 @@ import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
export type ActionSpec = {
|
||||||
|
text: string;
|
||||||
|
action: () => unknown;
|
||||||
|
style?: 'affirmative' | 'negative';
|
||||||
|
};
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
readonly children: React.ReactNode;
|
readonly children: React.ReactNode;
|
||||||
readonly affirmativeText?: string;
|
readonly title?: string | React.ReactNode;
|
||||||
readonly onAffirmative?: () => unknown;
|
readonly actions: Array<ActionSpec>;
|
||||||
readonly onClose: () => unknown;
|
readonly onClose: () => unknown;
|
||||||
readonly negativeText?: string;
|
|
||||||
readonly onNegative?: () => unknown;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = OwnProps;
|
export type Props = OwnProps;
|
||||||
|
@ -21,15 +25,7 @@ function focusRef(el: HTMLElement | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmationDialog = React.memo(
|
export const ConfirmationDialog = React.memo(
|
||||||
({
|
({ i18n, onClose, children, title, actions }: Props) => {
|
||||||
i18n,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
onAffirmative,
|
|
||||||
onNegative,
|
|
||||||
affirmativeText,
|
|
||||||
negativeText,
|
|
||||||
}: Props) => {
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handler = ({ key }: KeyboardEvent) => {
|
const handler = ({ key }: KeyboardEvent) => {
|
||||||
if (key === 'Escape') {
|
if (key === 'Escape') {
|
||||||
|
@ -52,22 +48,25 @@ export const ConfirmationDialog = React.memo(
|
||||||
[onClose]
|
[onClose]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNegative = React.useCallback(() => {
|
const handleAction = React.useCallback(
|
||||||
onClose();
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (onNegative) {
|
onClose();
|
||||||
onNegative();
|
if (e.currentTarget.dataset.action) {
|
||||||
}
|
const actionIndex = parseInt(e.currentTarget.dataset.action, 10);
|
||||||
}, [onClose, onNegative]);
|
const { action } = actions[actionIndex];
|
||||||
|
action();
|
||||||
const handleAffirmative = React.useCallback(() => {
|
}
|
||||||
onClose();
|
},
|
||||||
if (onAffirmative) {
|
[onClose, actions]
|
||||||
onAffirmative();
|
);
|
||||||
}
|
|
||||||
}, [onClose, onAffirmative]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-confirmation-dialog__container">
|
<div className="module-confirmation-dialog__container">
|
||||||
|
{title ? (
|
||||||
|
<h1 className="module-confirmation-dialog__container__title">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
) : null}
|
||||||
<div className="module-confirmation-dialog__container__content">
|
<div className="module-confirmation-dialog__container__content">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,28 +78,24 @@ export const ConfirmationDialog = React.memo(
|
||||||
>
|
>
|
||||||
{i18n('confirmation-dialog--Cancel')}
|
{i18n('confirmation-dialog--Cancel')}
|
||||||
</button>
|
</button>
|
||||||
{onNegative && negativeText ? (
|
{actions.map((action, i) => (
|
||||||
<button
|
<button
|
||||||
onClick={handleNegative}
|
key={i}
|
||||||
|
onClick={handleAction}
|
||||||
|
data-action={i}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-confirmation-dialog__container__buttons__button',
|
'module-confirmation-dialog__container__buttons__button',
|
||||||
'module-confirmation-dialog__container__buttons__button--negative'
|
action.style === 'affirmative'
|
||||||
|
? 'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||||
|
: null,
|
||||||
|
action.style === 'negative'
|
||||||
|
? 'module-confirmation-dialog__container__buttons__button--negative'
|
||||||
|
: null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{negativeText}
|
{action.text}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
))}
|
||||||
{onAffirmative && affirmativeText ? (
|
|
||||||
<button
|
|
||||||
onClick={handleAffirmative}
|
|
||||||
className={classNames(
|
|
||||||
'module-confirmation-dialog__container__buttons__button',
|
|
||||||
'module-confirmation-dialog__container__buttons__button--affirmative'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{affirmativeText}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,31 +1,21 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import {
|
||||||
|
ConfirmationDialog,
|
||||||
|
Props as ConfirmationDialogProps,
|
||||||
|
} from './ConfirmationDialog';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
readonly children: React.ReactNode;
|
|
||||||
readonly affirmativeText?: string;
|
|
||||||
readonly onAffirmative?: () => unknown;
|
|
||||||
readonly onClose: () => unknown;
|
readonly onClose: () => unknown;
|
||||||
readonly negativeText?: string;
|
|
||||||
readonly onNegative?: () => unknown;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = OwnProps;
|
export type Props = OwnProps & ConfirmationDialogProps;
|
||||||
|
|
||||||
export const ConfirmationModal = React.memo(
|
export const ConfirmationModal = React.memo(
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
({
|
({ i18n, onClose, children, ...rest }: Props) => {
|
||||||
i18n,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
onAffirmative,
|
|
||||||
onNegative,
|
|
||||||
affirmativeText,
|
|
||||||
negativeText,
|
|
||||||
}: Props) => {
|
|
||||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -72,14 +62,7 @@ export const ConfirmationModal = React.memo(
|
||||||
className="module-confirmation-dialog__overlay"
|
className="module-confirmation-dialog__overlay"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
>
|
>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog i18n={i18n} {...rest} onClose={onClose}>
|
||||||
i18n={i18n}
|
|
||||||
onClose={onClose}
|
|
||||||
onAffirmative={onAffirmative}
|
|
||||||
onNegative={onNegative}
|
|
||||||
affirmativeText={affirmativeText}
|
|
||||||
negativeText={negativeText}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
|
@ -25,7 +25,7 @@ export interface PropsType {
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
color: ColorType;
|
color?: ColorType;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { Emojify } from './Emojify';
|
import { Emojify } from './Emojify';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
|
|
|
@ -64,6 +64,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
phoneNumber: '(202) 555-0001',
|
phoneNumber: '(202) 555-0001',
|
||||||
id: '1',
|
id: '1',
|
||||||
profileName: '🔥Flames🔥',
|
profileName: '🔥Flames🔥',
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -76,6 +77,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
name: 'Someone 🔥 Somewhere',
|
name: 'Someone 🔥 Somewhere',
|
||||||
phoneNumber: '(202) 555-0002',
|
phoneNumber: '(202) 555-0002',
|
||||||
id: '2',
|
id: '2',
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -88,6 +90,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
phoneNumber: '(202) 555-0003',
|
phoneNumber: '(202) 555-0003',
|
||||||
id: '3',
|
id: '3',
|
||||||
profileName: '🔥Flames🔥',
|
profileName: '🔥Flames🔥',
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -97,6 +100,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
props: {
|
props: {
|
||||||
phoneNumber: '(202) 555-0011',
|
phoneNumber: '(202) 555-0011',
|
||||||
id: '11',
|
id: '11',
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -108,6 +112,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
color: 'deep_orange',
|
color: 'deep_orange',
|
||||||
phoneNumber: '(202) 555-0004',
|
phoneNumber: '(202) 555-0004',
|
||||||
id: '4',
|
id: '4',
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -129,6 +134,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
value: 10,
|
value: 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -159,6 +165,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
value: 10,
|
value: 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -183,6 +190,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
value: 10,
|
value: 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
isAccepted: true,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
@ -200,6 +208,25 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
phoneNumber: '(202) 555-0007',
|
phoneNumber: '(202) 555-0007',
|
||||||
id: '7',
|
id: '7',
|
||||||
isMe: true,
|
isMe: true,
|
||||||
|
isAccepted: true,
|
||||||
|
...actionProps,
|
||||||
|
...housekeepingProps,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Unaccepted',
|
||||||
|
description: 'No safety number entry.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: '1:1 conversation',
|
||||||
|
props: {
|
||||||
|
color: 'blue',
|
||||||
|
phoneNumber: '(202) 555-0007',
|
||||||
|
id: '7',
|
||||||
|
isMe: false,
|
||||||
|
isAccepted: false,
|
||||||
...actionProps,
|
...actionProps,
|
||||||
...housekeepingProps,
|
...housekeepingProps,
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface PropsData {
|
||||||
color?: ColorType;
|
color?: ColorType;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
|
|
||||||
|
isAccepted?: boolean;
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
isMe?: boolean;
|
isMe?: boolean;
|
||||||
isGroup?: boolean;
|
isGroup?: boolean;
|
||||||
|
@ -222,6 +223,7 @@ export class ConversationHeader extends React.Component<Props> {
|
||||||
public renderMenu(triggerId: string) {
|
public renderMenu(triggerId: string) {
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
|
isAccepted,
|
||||||
isMe,
|
isMe,
|
||||||
isGroup,
|
isGroup,
|
||||||
isArchived,
|
isArchived,
|
||||||
|
@ -241,7 +243,7 @@ export class ConversationHeader extends React.Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu id={triggerId}>
|
<ContextMenu id={triggerId}>
|
||||||
{leftGroup ? null : (
|
{!leftGroup && isAccepted ? (
|
||||||
<SubMenu title={disappearingTitle}>
|
<SubMenu title={disappearingTitle}>
|
||||||
{(timerOptions || []).map(item => (
|
{(timerOptions || []).map(item => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -254,7 +256,7 @@ export class ConversationHeader extends React.Component<Props> {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
)}
|
) : null}
|
||||||
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
|
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
|
||||||
{isGroup ? (
|
{isGroup ? (
|
||||||
<MenuItem onClick={onShowGroupMembers}>
|
<MenuItem onClick={onShowGroupMembers}>
|
||||||
|
@ -266,7 +268,7 @@ export class ConversationHeader extends React.Component<Props> {
|
||||||
{i18n('showSafetyNumber')}
|
{i18n('showSafetyNumber')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
{!isGroup ? (
|
{!isGroup && isAccepted ? (
|
||||||
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
|
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
{isArchived ? (
|
{isArchived ? (
|
||||||
|
|
130
ts/components/conversation/ConversationHero.stories.tsx
Normal file
130
ts/components/conversation/ConversationHero.stories.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { number as numberKnob, text } from '@storybook/addon-knobs';
|
||||||
|
import { ConversationHero } from './ConversationHero';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const getName = () => text('name', 'Cayce Bollard');
|
||||||
|
const getProfileName = () => text('profileName', 'Cayce Bollard');
|
||||||
|
const getAvatarPath = () =>
|
||||||
|
text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
|
||||||
|
const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700');
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
|
.add('Direct (Three Other Groups)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
avatarPath={getAvatarPath()}
|
||||||
|
name={getName()}
|
||||||
|
profileName={getProfileName()}
|
||||||
|
phoneNumber={getPhoneNumber()}
|
||||||
|
conversationType="direct"
|
||||||
|
groups={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Direct (Two Other Groups)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
avatarPath={getAvatarPath()}
|
||||||
|
name={getName()}
|
||||||
|
profileName={getProfileName()}
|
||||||
|
phoneNumber={getPhoneNumber()}
|
||||||
|
conversationType="direct"
|
||||||
|
groups={['NYC Rock Climbers', 'Dinner Party']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Direct (One Other Group)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
avatarPath={getAvatarPath()}
|
||||||
|
name={getName()}
|
||||||
|
profileName={getProfileName()}
|
||||||
|
phoneNumber={getPhoneNumber()}
|
||||||
|
conversationType="direct"
|
||||||
|
groups={['NYC Rock Climbers']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Direct (No Other Groups)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
avatarPath={getAvatarPath()}
|
||||||
|
name={getName()}
|
||||||
|
profileName={getProfileName()}
|
||||||
|
phoneNumber={getPhoneNumber()}
|
||||||
|
conversationType="direct"
|
||||||
|
groups={[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Group (many members)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
name={text('groupName', 'NYC Rock Climbers')}
|
||||||
|
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
|
||||||
|
conversationType="group"
|
||||||
|
membersCount={numberKnob('membersCount', 22)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Group (one member)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
name={text('groupName', 'NYC Rock Climbers')}
|
||||||
|
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
|
||||||
|
conversationType="group"
|
||||||
|
membersCount={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Group (zero members)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
name={text('groupName', 'NYC Rock Climbers')}
|
||||||
|
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
|
||||||
|
conversationType="group"
|
||||||
|
membersCount={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Note to Self', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<ConversationHero
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={true}
|
||||||
|
conversationType="direct"
|
||||||
|
phoneNumber={getPhoneNumber()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
125
ts/components/conversation/ConversationHero.tsx
Normal file
125
ts/components/conversation/ConversationHero.tsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { take } from 'lodash';
|
||||||
|
import { Avatar, Props as AvatarProps } from '../Avatar';
|
||||||
|
import { ContactName } from './ContactName';
|
||||||
|
import { Emojify } from './Emojify';
|
||||||
|
import { Intl } from '../Intl';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
isMe?: boolean;
|
||||||
|
groups?: Array<string>;
|
||||||
|
membersCount?: number;
|
||||||
|
phoneNumber: string;
|
||||||
|
onHeightChange?: () => unknown;
|
||||||
|
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||||
|
|
||||||
|
const renderMembershipRow = ({
|
||||||
|
i18n,
|
||||||
|
groups,
|
||||||
|
conversationType,
|
||||||
|
isMe,
|
||||||
|
}: Pick<Props, 'i18n' | 'groups' | 'conversationType' | 'isMe'>) => {
|
||||||
|
const className = 'module-conversation-hero__membership';
|
||||||
|
const nameClassName = `${className}__name`;
|
||||||
|
|
||||||
|
if (isMe) {
|
||||||
|
return <div className={className}>{i18n('noteToSelfHero')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationType === 'direct' && groups && groups.length > 0) {
|
||||||
|
const firstThreeGroups = take(groups, 3).map((group, i) => (
|
||||||
|
<strong key={i} className={nameClassName}>
|
||||||
|
<Emojify text={group} />
|
||||||
|
</strong>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id={`ConversationHero--membership-${firstThreeGroups.length}`}
|
||||||
|
components={firstThreeGroups}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConversationHero = ({
|
||||||
|
i18n,
|
||||||
|
avatarPath,
|
||||||
|
color,
|
||||||
|
conversationType,
|
||||||
|
isMe,
|
||||||
|
membersCount,
|
||||||
|
groups = [],
|
||||||
|
name,
|
||||||
|
phoneNumber,
|
||||||
|
profileName,
|
||||||
|
onHeightChange,
|
||||||
|
}: Props) => {
|
||||||
|
const firstRenderRef = React.useRef(true);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// If any of the depenencies for this hook change then the height of this
|
||||||
|
// component may have changed. The cleanup function notifies listeners of
|
||||||
|
// any potential height changes.
|
||||||
|
return () => {
|
||||||
|
if (onHeightChange && !firstRenderRef.current) {
|
||||||
|
onHeightChange();
|
||||||
|
} else {
|
||||||
|
firstRenderRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
firstRenderRef,
|
||||||
|
onHeightChange,
|
||||||
|
// Avoid collisions in these dependencies by prefixing them
|
||||||
|
// These dependencies may be dynamic, and therefore may cause height changes
|
||||||
|
`mc-${membersCount}`,
|
||||||
|
`n-${name}`,
|
||||||
|
`pn-${profileName}`,
|
||||||
|
...groups.map(g => `g-${g}`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation-hero">
|
||||||
|
<Avatar
|
||||||
|
i18n={i18n}
|
||||||
|
color={color}
|
||||||
|
noteToSelf={isMe}
|
||||||
|
avatarPath={avatarPath}
|
||||||
|
conversationType={conversationType}
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
size={112}
|
||||||
|
className="module-conversation-hero__avatar"
|
||||||
|
/>
|
||||||
|
<h1 className="module-conversation-hero__profile-name">
|
||||||
|
{isMe ? (
|
||||||
|
i18n('noteToSelf')
|
||||||
|
) : (
|
||||||
|
<ContactName
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
{!isMe ? (
|
||||||
|
<div className="module-conversation-hero__with">
|
||||||
|
{membersCount === 1
|
||||||
|
? i18n('ConversationHero--members-1')
|
||||||
|
: membersCount !== undefined
|
||||||
|
? i18n('ConversationHero--members', [`${membersCount}`])
|
||||||
|
: phoneNumber}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{renderMembershipRow({ isMe, groups, conversationType, i18n })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Blurhash } from 'react-blurhash';
|
||||||
|
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
@ -30,6 +31,7 @@ interface Props {
|
||||||
darkOverlay?: boolean;
|
darkOverlay?: boolean;
|
||||||
playIconOverlay?: boolean;
|
playIconOverlay?: boolean;
|
||||||
softCorners?: boolean;
|
softCorners?: boolean;
|
||||||
|
blurHash?: string;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onClick?: (attachment: AttachmentType) => void;
|
onClick?: (attachment: AttachmentType) => void;
|
||||||
|
@ -38,7 +40,21 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Image extends React.Component<Props> {
|
export class Image extends React.Component<Props> {
|
||||||
|
private canClick() {
|
||||||
|
const { onClick, attachment, url } = this.props;
|
||||||
|
const { pending } = attachment || { pending: true };
|
||||||
|
|
||||||
|
return Boolean(onClick && !pending && url);
|
||||||
|
}
|
||||||
|
|
||||||
public handleClick = (event: React.MouseEvent) => {
|
public handleClick = (event: React.MouseEvent) => {
|
||||||
|
if (!this.canClick()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { onClick, attachment } = this.props;
|
const { onClick, attachment } = this.props;
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
@ -50,6 +66,13 @@ export class Image extends React.Component<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (!this.canClick()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { onClick, attachment } = this.props;
|
const { onClick, attachment } = this.props;
|
||||||
|
|
||||||
if (onClick && (event.key === 'Enter' || event.key === 'Space')) {
|
if (onClick && (event.key === 'Enter' || event.key === 'Space')) {
|
||||||
|
@ -64,6 +87,7 @@ export class Image extends React.Component<Props> {
|
||||||
const {
|
const {
|
||||||
alt,
|
alt,
|
||||||
attachment,
|
attachment,
|
||||||
|
blurHash,
|
||||||
bottomOverlay,
|
bottomOverlay,
|
||||||
closeButton,
|
closeButton,
|
||||||
curveBottomLeft,
|
curveBottomLeft,
|
||||||
|
@ -71,11 +95,10 @@ export class Image extends React.Component<Props> {
|
||||||
curveTopLeft,
|
curveTopLeft,
|
||||||
curveTopRight,
|
curveTopRight,
|
||||||
darkOverlay,
|
darkOverlay,
|
||||||
height,
|
height = 0,
|
||||||
i18n,
|
i18n,
|
||||||
noBackground,
|
noBackground,
|
||||||
noBorder,
|
noBorder,
|
||||||
onClick,
|
|
||||||
onClickClose,
|
onClickClose,
|
||||||
onError,
|
onError,
|
||||||
overlayText,
|
overlayText,
|
||||||
|
@ -84,18 +107,16 @@ export class Image extends React.Component<Props> {
|
||||||
softCorners,
|
softCorners,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
url,
|
url,
|
||||||
width,
|
width = 0,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { caption, pending } = attachment || { caption: null, pending: true };
|
const { caption, pending } = attachment || { caption: null, pending: true };
|
||||||
const canClick = onClick && !pending;
|
const canClick = this.canClick();
|
||||||
|
|
||||||
const overlayClassName = classNames(
|
const overlayClassName = classNames(
|
||||||
'module-image__border-overlay',
|
'module-image__border-overlay',
|
||||||
noBorder ? null : 'module-image__border-overlay--with-border',
|
noBorder ? null : 'module-image__border-overlay--with-border',
|
||||||
canClick && onClick
|
canClick ? 'module-image__border-overlay--with-click-handler' : null,
|
||||||
? 'module-image__border-overlay--with-click-handler'
|
|
||||||
: null,
|
|
||||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||||
|
@ -105,19 +126,14 @@ export class Image extends React.Component<Props> {
|
||||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||||
);
|
);
|
||||||
|
|
||||||
let overlay;
|
const overlay = canClick ? (
|
||||||
if (canClick && onClick) {
|
<button
|
||||||
overlay = (
|
className={overlayClassName}
|
||||||
<button
|
onClick={this.handleClick}
|
||||||
className={overlayClassName}
|
onKeyDown={this.handleKeyDown}
|
||||||
onClick={this.handleClick}
|
tabIndex={tabIndex}
|
||||||
onKeyDown={this.handleKeyDown}
|
/>
|
||||||
tabIndex={tabIndex}
|
) : null;
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
overlay = <div className={overlayClassName} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -145,7 +161,7 @@ export class Image extends React.Component<Props> {
|
||||||
>
|
>
|
||||||
<Spinner svgSize="normal" />
|
<Spinner svgSize="normal" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : url ? (
|
||||||
<img
|
<img
|
||||||
onError={onError}
|
onError={onError}
|
||||||
className="module-image__image"
|
className="module-image__image"
|
||||||
|
@ -154,7 +170,14 @@ export class Image extends React.Component<Props> {
|
||||||
width={width}
|
width={width}
|
||||||
src={url}
|
src={url}
|
||||||
/>
|
/>
|
||||||
)}
|
) : blurHash ? (
|
||||||
|
<Blurhash
|
||||||
|
hash={blurHash}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{caption ? (
|
{caption ? (
|
||||||
<img
|
<img
|
||||||
className="module-image__caption-icon"
|
className="module-image__caption-icon"
|
||||||
|
|
|
@ -76,6 +76,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[0].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={isSticker}
|
noBorder={isSticker}
|
||||||
noBackground={isSticker}
|
noBackground={isSticker}
|
||||||
|
@ -103,6 +104,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
|
blurHash={attachments[0].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
|
@ -117,6 +119,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[1], i18n)}
|
alt={getAlt(attachments[1], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[1].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
curveTopRight={curveTopRight}
|
curveTopRight={curveTopRight}
|
||||||
|
@ -139,6 +142,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[0].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
|
@ -155,6 +159,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[1], i18n)}
|
alt={getAlt(attachments[1], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[1].blurHash}
|
||||||
curveTopRight={curveTopRight}
|
curveTopRight={curveTopRight}
|
||||||
height={99}
|
height={99}
|
||||||
width={99}
|
width={99}
|
||||||
|
@ -167,6 +172,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[2], i18n)}
|
alt={getAlt(attachments[2], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[2].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
|
@ -191,6 +197,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[0].blurHash}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
|
@ -204,6 +211,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[1], i18n)}
|
alt={getAlt(attachments[1], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[1].blurHash}
|
||||||
curveTopRight={curveTopRight}
|
curveTopRight={curveTopRight}
|
||||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
|
@ -219,6 +227,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[2], i18n)}
|
alt={getAlt(attachments[2], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[2].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
|
@ -233,6 +242,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[3], i18n)}
|
alt={getAlt(attachments[3], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[3].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={false}
|
noBorder={false}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
|
@ -262,6 +272,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[0].blurHash}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
|
@ -274,6 +285,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[1], i18n)}
|
alt={getAlt(attachments[1], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[1].blurHash}
|
||||||
curveTopRight={curveTopRight}
|
curveTopRight={curveTopRight}
|
||||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||||
height={149}
|
height={149}
|
||||||
|
@ -288,6 +300,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[2], i18n)}
|
alt={getAlt(attachments[2], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[2].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={isSticker}
|
noBorder={isSticker}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
|
@ -302,6 +315,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[3], i18n)}
|
alt={getAlt(attachments[3], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[3].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={isSticker}
|
noBorder={isSticker}
|
||||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||||
|
@ -315,6 +329,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[4], i18n)}
|
alt={getAlt(attachments[4], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
blurHash={attachments[4].blurHash}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
noBorder={isSticker}
|
noBorder={isSticker}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
PropsHousekeeping,
|
PropsHousekeeping,
|
||||||
} from './Message';
|
} from './Message';
|
||||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
|
import { MIMEType } from '../../types/MIME';
|
||||||
|
|
||||||
const book = storiesOf('Components/Conversation/Message', module);
|
const book = storiesOf('Components/Conversation/Message', module);
|
||||||
|
|
||||||
|
@ -1272,6 +1273,28 @@ const stories: Array<MessageStory> = [
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'BlurHash',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: 'Incoming BlurHash',
|
||||||
|
makeDataProps: () => ({
|
||||||
|
...baseDataProps,
|
||||||
|
direction: 'incoming',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
blurHash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',
|
||||||
|
width: 300,
|
||||||
|
height: 600,
|
||||||
|
fileName: 'foo.jpg',
|
||||||
|
contentType: 'image/jpeg' as MIMEType,
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderEmojiPicker: AllProps['renderEmojiPicker'] = ({
|
const renderEmojiPicker: AllProps['renderEmojiPicker'] = ({
|
||||||
|
|
|
@ -573,6 +573,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
withContentBelow
|
withContentBelow
|
||||||
? 'module-message__attachment-container--with-content-below'
|
? 'module-message__attachment-container--with-content-below'
|
||||||
: null,
|
: null,
|
||||||
|
collapseMetadata
|
||||||
|
? 'module-message__attachment-container--with-collapsed-metadata'
|
||||||
|
: null,
|
||||||
isSticker && !collapseMetadata
|
isSticker && !collapseMetadata
|
||||||
? 'module-message__sticker-container--with-content-below'
|
? 'module-message__sticker-container--with-content-below'
|
||||||
: null
|
: null
|
||||||
|
@ -627,6 +630,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
: null,
|
: null,
|
||||||
withContentAbove
|
withContentAbove
|
||||||
? 'module-message__generic-attachment--with-content-above'
|
? 'module-message__generic-attachment--with-content-above'
|
||||||
|
: null,
|
||||||
|
!firstAttachment.url
|
||||||
|
? 'module-message__generic-attachment--not-active'
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
// There's only ever one of these, so we don't want users to tab into it
|
// There's only ever one of these, so we don't want users to tab into it
|
||||||
|
@ -635,6 +641,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!firstAttachment.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.openGenericAttachment();
|
this.openGenericAttachment();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1117,7 +1127,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{canReply ? reactButton : null}
|
{canReply ? reactButton : null}
|
||||||
{downloadButton}
|
{canReply ? downloadButton : null}
|
||||||
{canReply ? replyButton : null}
|
{canReply ? replyButton : null}
|
||||||
{menuButton}
|
{menuButton}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1881,6 +1891,15 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there an incomplete attachment, do not execute the default action
|
||||||
|
const { attachments } = this.props;
|
||||||
|
if (attachments && attachments.length > 0) {
|
||||||
|
const [firstAttachment] = attachments;
|
||||||
|
if (!firstAttachment.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.handleOpen(event);
|
this.handleOpen(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
59
ts/components/conversation/MessageRequestActions.stories.tsx
Normal file
59
ts/components/conversation/MessageRequestActions.stories.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text } from '@storybook/addon-knobs';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import {
|
||||||
|
MessageRequestActions,
|
||||||
|
Props as MessageRequestActionsProps,
|
||||||
|
} from './MessageRequestActions';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const getBaseProps = (isGroup = false): MessageRequestActionsProps => ({
|
||||||
|
i18n,
|
||||||
|
conversationType: isGroup ? 'group' : 'direct',
|
||||||
|
profileName: isGroup ? undefined : text('profileName', 'Cayce Bollard'),
|
||||||
|
name: isGroup
|
||||||
|
? text('name', 'NYC Rock Climbers')
|
||||||
|
: text('name', 'Cayce Bollard'),
|
||||||
|
onBlock: action('block'),
|
||||||
|
onDelete: action('delete'),
|
||||||
|
onBlockAndDelete: action('blockAndDelete'),
|
||||||
|
onUnblock: action('unblock'),
|
||||||
|
onAccept: action('accept'),
|
||||||
|
});
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/MessageRequestActions', module)
|
||||||
|
.add('Direct', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<MessageRequestActions {...getBaseProps()} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Direct (Blocked)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<MessageRequestActions {...getBaseProps()} isBlocked={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Group', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<MessageRequestActions {...getBaseProps(true)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Group (Blocked)', () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '480px' }}>
|
||||||
|
<MessageRequestActions {...getBaseProps(true)} isBlocked={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
130
ts/components/conversation/MessageRequestActions.tsx
Normal file
130
ts/components/conversation/MessageRequestActions.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { ContactName, Props as ContactNameProps } from './ContactName';
|
||||||
|
import {
|
||||||
|
MessageRequestActionsConfirmation,
|
||||||
|
MessageRequestState,
|
||||||
|
Props as MessageRequestActionsConfirmationProps,
|
||||||
|
} from './MessageRequestActionsConfirmation';
|
||||||
|
import { Intl } from '../Intl';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
onAccept(): unknown;
|
||||||
|
} & Omit<ContactNameProps, 'module'> &
|
||||||
|
Omit<
|
||||||
|
MessageRequestActionsConfirmationProps,
|
||||||
|
'i18n' | 'state' | 'onChangeState'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const MessageRequestActions = ({
|
||||||
|
i18n,
|
||||||
|
name,
|
||||||
|
profileName,
|
||||||
|
phoneNumber,
|
||||||
|
conversationType,
|
||||||
|
isBlocked,
|
||||||
|
onBlock,
|
||||||
|
onBlockAndDelete,
|
||||||
|
onUnblock,
|
||||||
|
onDelete,
|
||||||
|
onAccept,
|
||||||
|
}: Props) => {
|
||||||
|
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mrState !== MessageRequestState.default ? (
|
||||||
|
<MessageRequestActionsConfirmation
|
||||||
|
i18n={i18n}
|
||||||
|
onBlock={onBlock}
|
||||||
|
onBlockAndDelete={onBlockAndDelete}
|
||||||
|
onUnblock={onUnblock}
|
||||||
|
onDelete={onDelete}
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
conversationType={conversationType}
|
||||||
|
state={mrState}
|
||||||
|
onChangeState={setMrState}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="module-message-request-actions">
|
||||||
|
<p className="module-message-request-actions__message">
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id={`MessageRequests--message-${conversationType}${
|
||||||
|
isBlocked ? '-blocked' : ''
|
||||||
|
}`}
|
||||||
|
components={[
|
||||||
|
<strong
|
||||||
|
key="name"
|
||||||
|
className="module-message-request-actions__message__name"
|
||||||
|
>
|
||||||
|
<ContactName
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
/>
|
||||||
|
</strong>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div className="module-message-request-actions__buttons">
|
||||||
|
{isBlocked ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setMrState(MessageRequestState.unblocking);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
'module-message-request-actions__buttons__button',
|
||||||
|
'module-message-request-actions__buttons__button--accept'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('MessageRequests--unblock')}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setMrState(MessageRequestState.blocking);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
'module-message-request-actions__buttons__button',
|
||||||
|
'module-message-request-actions__buttons__button--deny'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('MessageRequests--block')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setMrState(MessageRequestState.deleting);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
'module-message-request-actions__buttons__button',
|
||||||
|
'module-message-request-actions__buttons__button--deny'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('MessageRequests--delete')}
|
||||||
|
</button>
|
||||||
|
{!isBlocked ? (
|
||||||
|
<button
|
||||||
|
onClick={onAccept}
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
'module-message-request-actions__buttons__button',
|
||||||
|
'module-message-request-actions__buttons__button--accept'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('MessageRequests--accept')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
156
ts/components/conversation/MessageRequestActionsConfirmation.tsx
Normal file
156
ts/components/conversation/MessageRequestActionsConfirmation.tsx
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ContactName, Props as ContactNameProps } from './ContactName';
|
||||||
|
import { ConfirmationModal } from '../ConfirmationModal';
|
||||||
|
import { Intl } from '../Intl';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export enum MessageRequestState {
|
||||||
|
blocking,
|
||||||
|
deleting,
|
||||||
|
unblocking,
|
||||||
|
default,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
conversationType: 'group' | 'direct';
|
||||||
|
isBlocked?: boolean;
|
||||||
|
onBlock(): unknown;
|
||||||
|
onBlockAndDelete(): unknown;
|
||||||
|
onUnblock(): unknown;
|
||||||
|
onDelete(): unknown;
|
||||||
|
state: MessageRequestState;
|
||||||
|
onChangeState(state: MessageRequestState): unknown;
|
||||||
|
} & Omit<ContactNameProps, 'module'>;
|
||||||
|
|
||||||
|
// tslint:disable-next-line: max-func-body-length
|
||||||
|
export const MessageRequestActionsConfirmation = ({
|
||||||
|
i18n,
|
||||||
|
name,
|
||||||
|
profileName,
|
||||||
|
phoneNumber,
|
||||||
|
conversationType,
|
||||||
|
onBlock,
|
||||||
|
onBlockAndDelete,
|
||||||
|
onUnblock,
|
||||||
|
onDelete,
|
||||||
|
state,
|
||||||
|
onChangeState,
|
||||||
|
}: Props) => {
|
||||||
|
if (state === MessageRequestState.blocking) {
|
||||||
|
return (
|
||||||
|
// tslint:disable-next-line: use-simple-attributes
|
||||||
|
<ConfirmationModal
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => {
|
||||||
|
onChangeState(MessageRequestState.default);
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id={`MessageRequests--block-${conversationType}-confirm-title`}
|
||||||
|
components={[
|
||||||
|
<ContactName
|
||||||
|
key="name"
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: i18n('MessageRequests--block'),
|
||||||
|
action: onBlock,
|
||||||
|
style: 'negative',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n('MessageRequests--block-and-delete'),
|
||||||
|
action: onBlockAndDelete,
|
||||||
|
style: 'negative',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{i18n(`MessageRequests--block-${conversationType}-confirm-body`)}
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === MessageRequestState.unblocking) {
|
||||||
|
return (
|
||||||
|
// tslint:disable-next-line: use-simple-attributes
|
||||||
|
<ConfirmationModal
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => {
|
||||||
|
onChangeState(MessageRequestState.default);
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id={'MessageRequests--unblock-confirm-title'}
|
||||||
|
components={[
|
||||||
|
<ContactName
|
||||||
|
key="name"
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: i18n('MessageRequests--unblock'),
|
||||||
|
action: onUnblock,
|
||||||
|
style: 'affirmative',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n('MessageRequests--delete'),
|
||||||
|
action: onDelete,
|
||||||
|
style: 'negative',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{i18n(`MessageRequests--unblock-${conversationType}-confirm-body`)}
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === MessageRequestState.deleting) {
|
||||||
|
return (
|
||||||
|
// tslint:disable-next-line: use-simple-attributes
|
||||||
|
<ConfirmationModal
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => {
|
||||||
|
onChangeState(MessageRequestState.default);
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id={`MessageRequests--delete-${conversationType}-confirm-title`}
|
||||||
|
components={[
|
||||||
|
<ContactName
|
||||||
|
key="name"
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: i18n(`MessageRequests--delete-${conversationType}`),
|
||||||
|
action: onDelete,
|
||||||
|
style: 'negative',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{i18n(`MessageRequests--delete-${conversationType}-confirm-body`)}
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -50,6 +50,7 @@ type PropsHousekeepingType = {
|
||||||
actions: Object
|
actions: Object
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||||
|
renderHeroRow: (id: string, resizeHeroRow: () => unknown) => JSX.Element;
|
||||||
renderLoadingRow: (id: string) => JSX.Element;
|
renderLoadingRow: (id: string) => JSX.Element;
|
||||||
renderTypingBubble: (id: string) => JSX.Element;
|
renderTypingBubble: (id: string) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
@ -250,6 +251,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
this.recomputeRowHeights(row || 0);
|
this.recomputeRowHeights(row || 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public resizeHeroRow = () => {
|
||||||
|
this.resize(0);
|
||||||
|
};
|
||||||
|
|
||||||
public onScroll = (data: OnScrollParamsType) => {
|
public onScroll = (data: OnScrollParamsType) => {
|
||||||
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||||
// re-measures to get us where we want to go.
|
// re-measures to get us where we want to go.
|
||||||
|
@ -501,6 +506,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
haveOldest,
|
haveOldest,
|
||||||
items,
|
items,
|
||||||
renderItem,
|
renderItem,
|
||||||
|
renderHeroRow,
|
||||||
renderLoadingRow,
|
renderLoadingRow,
|
||||||
renderLastSeenIndicator,
|
renderLastSeenIndicator,
|
||||||
renderTypingBubble,
|
renderTypingBubble,
|
||||||
|
@ -515,7 +521,13 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
const typingBubbleRow = this.getTypingBubbleRow();
|
const typingBubbleRow = this.getTypingBubbleRow();
|
||||||
let rowContents;
|
let rowContents;
|
||||||
|
|
||||||
if (!haveOldest && row === 0) {
|
if (haveOldest && row === 0) {
|
||||||
|
rowContents = (
|
||||||
|
<div data-row={row} style={styleWithWidth} role="row">
|
||||||
|
{renderHeroRow(id, this.resizeHeroRow)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!haveOldest && row === 0) {
|
||||||
rowContents = (
|
rowContents = (
|
||||||
<div data-row={row} style={styleWithWidth} role="row">
|
<div data-row={row} style={styleWithWidth} role="row">
|
||||||
{renderLoadingRow(id)}
|
{renderLoadingRow(id)}
|
||||||
|
@ -574,13 +586,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public fromItemIndexToRow(index: number) {
|
public fromItemIndexToRow(index: number) {
|
||||||
const { haveOldest, oldestUnreadIndex } = this.props;
|
const { oldestUnreadIndex } = this.props;
|
||||||
|
|
||||||
let addition = 0;
|
// We will always render either the hero row or the loading row
|
||||||
|
let addition = 1;
|
||||||
if (!haveOldest) {
|
|
||||||
addition += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNumber(oldestUnreadIndex) && index >= oldestUnreadIndex) {
|
if (isNumber(oldestUnreadIndex) && index >= oldestUnreadIndex) {
|
||||||
addition += 1;
|
addition += 1;
|
||||||
|
@ -590,15 +599,12 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRowCount() {
|
public getRowCount() {
|
||||||
const { haveOldest, oldestUnreadIndex, typingContact } = this.props;
|
const { oldestUnreadIndex, typingContact } = this.props;
|
||||||
const { items } = this.props;
|
const { items } = this.props;
|
||||||
const itemsCount = items && items.length ? items.length : 0;
|
const itemsCount = items && items.length ? items.length : 0;
|
||||||
|
|
||||||
let extraRows = 0;
|
// We will always render either the hero row or the loading row
|
||||||
|
let extraRows = 1;
|
||||||
if (!haveOldest) {
|
|
||||||
extraRows += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNumber(oldestUnreadIndex)) {
|
if (isNumber(oldestUnreadIndex)) {
|
||||||
extraRows += 1;
|
extraRows += 1;
|
||||||
|
@ -612,13 +618,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public fromRowToItemIndex(row: number, props?: Props): number | undefined {
|
public fromRowToItemIndex(row: number, props?: Props): number | undefined {
|
||||||
const { haveOldest, items } = props || this.props;
|
const { items } = props || this.props;
|
||||||
|
|
||||||
let subtraction = 0;
|
// We will always render either the hero row or the loading row
|
||||||
|
let subtraction = 1;
|
||||||
if (!haveOldest) {
|
|
||||||
subtraction += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||||
if (isNumber(oldestUnreadRow) && row > oldestUnreadRow) {
|
if (isNumber(oldestUnreadRow) && row > oldestUnreadRow) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
|
||||||
|
|
||||||
const getDefaultProps = () => ({
|
const getDefaultProps = () => ({
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
|
conversationAccepted: true,
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
selectMessage: action('selectMessage'),
|
selectMessage: action('selectMessage'),
|
||||||
|
|
|
@ -77,6 +77,7 @@ export type TimelineItemType =
|
||||||
|
|
||||||
type PropsLocalType = {
|
type PropsLocalType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
conversationAccepted: boolean;
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
id: string;
|
id: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
|
|
@ -92,8 +92,13 @@ export const StickerManagerPackRow = React.memo(
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={clearUninstalling}
|
onClose={clearUninstalling}
|
||||||
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
actions={[
|
||||||
onNegative={handleConfirmUninstall}
|
{
|
||||||
|
style: 'negative',
|
||||||
|
text: i18n('stickers--StickerManager--Uninstall'),
|
||||||
|
action: handleConfirmUninstall,
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{i18n('stickers--StickerManager--UninstallWarning')}
|
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
|
|
|
@ -174,8 +174,13 @@ export const StickerPreviewModal = React.memo(
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
actions={[
|
||||||
onNegative={handleUninstall}
|
{
|
||||||
|
style: 'negative',
|
||||||
|
text: i18n('stickers--StickerManager--Uninstall'),
|
||||||
|
action: handleUninstall,
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{i18n('stickers--StickerManager--UninstallWarning')}
|
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
|
|
|
@ -852,8 +852,8 @@ async function searchMessagesInConversation(
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
|
|
||||||
async function getMessageCount() {
|
async function getMessageCount(conversationId?: string) {
|
||||||
return channels.getMessageCount();
|
return channels.getMessageCount(conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMessage(
|
async function saveMessage(
|
||||||
|
|
|
@ -85,7 +85,7 @@ export interface DataInterface {
|
||||||
options?: { limit?: number }
|
options?: { limit?: number }
|
||||||
) => Promise<Array<SearchResultMessageType>>;
|
) => Promise<Array<SearchResultMessageType>>;
|
||||||
|
|
||||||
getMessageCount: () => Promise<number>;
|
getMessageCount: (conversationId?: string) => Promise<number>;
|
||||||
saveMessages: (
|
saveMessages: (
|
||||||
arrayOfMessages: Array<MessageType>,
|
arrayOfMessages: Array<MessageType>,
|
||||||
options: { forceSave?: boolean }
|
options: { forceSave?: boolean }
|
||||||
|
|
|
@ -1542,6 +1542,40 @@ async function updateToSchemaVersion20(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateToSchemaVersion21(
|
||||||
|
currentVersion: number,
|
||||||
|
instance: PromisifiedSQLDatabase
|
||||||
|
) {
|
||||||
|
if (currentVersion >= 21) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await instance.run('BEGIN TRANSACTION;');
|
||||||
|
await instance.run(`
|
||||||
|
UPDATE conversations
|
||||||
|
SET json = json_set(
|
||||||
|
json,
|
||||||
|
'$.messageCount',
|
||||||
|
(SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await instance.run(`
|
||||||
|
UPDATE conversations
|
||||||
|
SET json = json_set(
|
||||||
|
json,
|
||||||
|
'$.sentMessageCount',
|
||||||
|
(SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id AND messages.type = 'outgoing')
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await instance.run('PRAGMA user_version = 21;');
|
||||||
|
await instance.run('COMMIT TRANSACTION;');
|
||||||
|
console.log('updateToSchemaVersion21: success!');
|
||||||
|
} catch (error) {
|
||||||
|
await instance.run('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SCHEMA_VERSIONS = [
|
const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1,
|
updateToSchemaVersion1,
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
|
@ -1563,6 +1597,7 @@ const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion18,
|
updateToSchemaVersion18,
|
||||||
updateToSchemaVersion19,
|
updateToSchemaVersion19,
|
||||||
updateToSchemaVersion20,
|
updateToSchemaVersion20,
|
||||||
|
updateToSchemaVersion21,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function updateSchema(instance: PromisifiedSQLDatabase) {
|
async function updateSchema(instance: PromisifiedSQLDatabase) {
|
||||||
|
@ -2326,9 +2361,14 @@ async function searchMessagesInConversation(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMessageCount() {
|
async function getMessageCount(conversationId?: string) {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const row = await db.get('SELECT count(*) from messages;');
|
const row = conversationId
|
||||||
|
? await db.get(
|
||||||
|
'SELECT count(*) from messages WHERE conversationId = $conversationId;',
|
||||||
|
{ $conversationId: conversationId }
|
||||||
|
)
|
||||||
|
: await db.get('SELECT count(*) from messages;');
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('getMessageCount: Unable to get count of messages');
|
throw new Error('getMessageCount: Unable to get count of messages');
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
import { NoopActionType } from './noop';
|
import { NoopActionType } from './noop';
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
|
import { ColorType } from '../../types/Util';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -24,7 +25,11 @@ export type DBConversationType = {
|
||||||
export type ConversationType = {
|
export type ConversationType = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
isArchived: boolean;
|
profileName?: string;
|
||||||
|
avatarPath?: string;
|
||||||
|
color?: ColorType;
|
||||||
|
isArchived?: boolean;
|
||||||
|
isBlocked?: boolean;
|
||||||
activeAt?: number;
|
activeAt?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
inboxPosition: number;
|
inboxPosition: number;
|
||||||
|
@ -33,6 +38,7 @@ export type ConversationType = {
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
|
membersCount?: number;
|
||||||
type: 'direct' | 'group';
|
type: 'direct' | 'group';
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
|
@ -49,6 +55,9 @@ export type ConversationType = {
|
||||||
shouldShowDraft?: boolean;
|
shouldShowDraft?: boolean;
|
||||||
draftText?: string;
|
draftText?: string;
|
||||||
draftPreview?: string;
|
draftPreview?: string;
|
||||||
|
|
||||||
|
messageRequestsEnabled?: boolean;
|
||||||
|
acceptedMessageRequest?: boolean;
|
||||||
};
|
};
|
||||||
export type ConversationLookupType = {
|
export type ConversationLookupType = {
|
||||||
[key: string]: ConversationType;
|
[key: string]: ConversationType;
|
||||||
|
|
|
@ -71,6 +71,14 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
recentStickers,
|
recentStickers,
|
||||||
showIntroduction,
|
showIntroduction,
|
||||||
showPickerHint,
|
showPickerHint,
|
||||||
|
// Message Requests
|
||||||
|
messageRequestsEnabled: conversation.messageRequestsEnabled,
|
||||||
|
acceptedMessageRequest: conversation.acceptedMessageRequest,
|
||||||
|
isBlocked: conversation.isBlocked,
|
||||||
|
conversationType: conversation.type,
|
||||||
|
name: conversation.name,
|
||||||
|
profileName: conversation.profileName,
|
||||||
|
phoneNumber: conversation.phoneNumber,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
37
ts/state/smart/HeroRow.tsx
Normal file
37
ts/state/smart/HeroRow.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
|
||||||
|
import { ConversationHero } from '../../components/conversation/ConversationHero';
|
||||||
|
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
const conversation = state.conversations.conversationLookup[id];
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Did not find conversation ${id} in state!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
i18n: getIntl(state),
|
||||||
|
avatarPath: conversation.avatarPath,
|
||||||
|
color: conversation.color,
|
||||||
|
conversationType: conversation.type,
|
||||||
|
isMe: conversation.isMe,
|
||||||
|
membersCount: conversation.membersCount,
|
||||||
|
name: conversation.name,
|
||||||
|
phoneNumber: conversation.phoneNumber,
|
||||||
|
profileName: conversation.profileName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartHeroRow = smart(ConversationHero);
|
|
@ -16,6 +16,7 @@ import {
|
||||||
import { SmartTimelineItem } from './TimelineItem';
|
import { SmartTimelineItem } from './TimelineItem';
|
||||||
import { SmartTypingBubble } from './TypingBubble';
|
import { SmartTypingBubble } from './TypingBubble';
|
||||||
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||||
|
import { SmartHeroRow } from './HeroRow';
|
||||||
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||||
import { SmartEmojiPicker } from './EmojiPicker';
|
import { SmartEmojiPicker } from './EmojiPicker';
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ import { SmartEmojiPicker } from './EmojiPicker';
|
||||||
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
||||||
const FilteredSmartTypingBubble = SmartTypingBubble as any;
|
const FilteredSmartTypingBubble = SmartTypingBubble as any;
|
||||||
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
|
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
|
||||||
|
const FilteredSmartHeroRow = SmartHeroRow as any;
|
||||||
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
|
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
|
@ -66,6 +68,9 @@ function renderEmojiPicker({
|
||||||
function renderLastSeenIndicator(id: string): JSX.Element {
|
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||||
return <FilteredSmartLastSeenIndicator id={id} />;
|
return <FilteredSmartLastSeenIndicator id={id} />;
|
||||||
}
|
}
|
||||||
|
function renderHeroRow(id: string, onHeightChange: () => unknown): JSX.Element {
|
||||||
|
return <FilteredSmartHeroRow id={id} onHeightChange={onHeightChange} />;
|
||||||
|
}
|
||||||
function renderLoadingRow(id: string): JSX.Element {
|
function renderLoadingRow(id: string): JSX.Element {
|
||||||
return <FilteredSmartTimelineLoadingRow id={id} />;
|
return <FilteredSmartTimelineLoadingRow id={id} />;
|
||||||
}
|
}
|
||||||
|
@ -88,6 +93,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
renderItem,
|
renderItem,
|
||||||
renderLastSeenIndicator,
|
renderLastSeenIndicator,
|
||||||
|
renderHeroRow,
|
||||||
renderLoadingRow,
|
renderLoadingRow,
|
||||||
renderTypingBubble,
|
renderTypingBubble,
|
||||||
...actions,
|
...actions,
|
||||||
|
|
|
@ -31,6 +31,8 @@ describe('state/selectors/conversations', () => {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
id2: {
|
id2: {
|
||||||
id: 'id2',
|
id: 'id2',
|
||||||
|
@ -51,6 +53,8 @@ describe('state/selectors/conversations', () => {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
id3: {
|
id3: {
|
||||||
id: 'id3',
|
id: 'id3',
|
||||||
|
@ -71,6 +75,8 @@ describe('state/selectors/conversations', () => {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
id4: {
|
id4: {
|
||||||
id: 'id4',
|
id: 'id4',
|
||||||
|
@ -91,6 +97,8 @@ describe('state/selectors/conversations', () => {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
id5: {
|
id5: {
|
||||||
id: 'id5',
|
id: 'id5',
|
||||||
|
@ -111,6 +119,8 @@ describe('state/selectors/conversations', () => {
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const comparator = _getConversationComparator(i18n, regionCode);
|
const comparator = _getConversationComparator(i18n, regionCode);
|
||||||
|
|
18
ts/textsecure.d.ts
vendored
18
ts/textsecure.d.ts
vendored
|
@ -531,6 +531,7 @@ export declare class SyncMessageClass {
|
||||||
padding?: ProtoBinaryType;
|
padding?: ProtoBinaryType;
|
||||||
stickerPackOperation?: Array<SyncMessageClass.StickerPackOperation>;
|
stickerPackOperation?: Array<SyncMessageClass.StickerPackOperation>;
|
||||||
viewOnceOpen?: SyncMessageClass.ViewOnceOpen;
|
viewOnceOpen?: SyncMessageClass.ViewOnceOpen;
|
||||||
|
messageRequestResponse?: SyncMessageClass.MessageRequestResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: we need to use namespaces to express nested classes in Typescript
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
@ -582,6 +583,13 @@ export declare namespace SyncMessageClass {
|
||||||
senderUuid?: string;
|
senderUuid?: string;
|
||||||
timestamp?: ProtoBinaryType;
|
timestamp?: ProtoBinaryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MessageRequestResponse {
|
||||||
|
threadE164?: string;
|
||||||
|
threadUuid?: string;
|
||||||
|
groupId?: ProtoBinaryType;
|
||||||
|
type?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: we need to use namespaces to express nested classes in Typescript
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
@ -612,6 +620,16 @@ export declare namespace SyncMessageClass.StickerPackOperation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare namespace SyncMessageClass.MessageRequestResponse {
|
||||||
|
class Type {
|
||||||
|
static UNKNOWN: number;
|
||||||
|
static ACCEPT: number;
|
||||||
|
static DELETE: number;
|
||||||
|
static BLOCK: number;
|
||||||
|
static BLOCK_AND_DELETE: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export declare class TypingMessageClass {
|
export declare class TypingMessageClass {
|
||||||
static decode: (
|
static decode: (
|
||||||
data: ArrayBuffer | ByteBufferClass,
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
|
|
@ -42,6 +42,8 @@ declare global {
|
||||||
deliveryReceipt?: any;
|
deliveryReceipt?: any;
|
||||||
error?: any;
|
error?: any;
|
||||||
groupDetails?: any;
|
groupDetails?: any;
|
||||||
|
groupId?: string;
|
||||||
|
messageRequestResponseType?: number;
|
||||||
proto?: any;
|
proto?: any;
|
||||||
read?: any;
|
read?: any;
|
||||||
reason?: any;
|
reason?: any;
|
||||||
|
@ -51,6 +53,8 @@ declare global {
|
||||||
source?: any;
|
source?: any;
|
||||||
sourceUuid?: any;
|
sourceUuid?: any;
|
||||||
stickerPacks?: any;
|
stickerPacks?: any;
|
||||||
|
threadE164?: string;
|
||||||
|
threadUuid?: string;
|
||||||
timestamp?: any;
|
timestamp?: any;
|
||||||
typing?: any;
|
typing?: any;
|
||||||
verified?: any;
|
verified?: any;
|
||||||
|
@ -1119,6 +1123,22 @@ class MessageReceiverInner extends EventTarget {
|
||||||
) {
|
) {
|
||||||
p = this.handleEndSession(destination);
|
p = this.handleEndSession(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
msg.flags &&
|
||||||
|
msg.flags &
|
||||||
|
window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE
|
||||||
|
) {
|
||||||
|
const ev = new Event('profileKeyUpdate');
|
||||||
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
|
ev.data = {
|
||||||
|
source: envelope.source,
|
||||||
|
sourceUuid: envelope.sourceUuid,
|
||||||
|
profileKey: msg.profileKey.toString('base64'),
|
||||||
|
};
|
||||||
|
return this.dispatchAndWait(ev);
|
||||||
|
}
|
||||||
|
|
||||||
return p.then(async () =>
|
return p.then(async () =>
|
||||||
this.processDecrypted(envelope, msg).then(message => {
|
this.processDecrypted(envelope, msg).then(message => {
|
||||||
const groupId = message.group && message.group.id;
|
const groupId = message.group && message.group.id;
|
||||||
|
@ -1373,6 +1393,11 @@ class MessageReceiverInner extends EventTarget {
|
||||||
);
|
);
|
||||||
} else if (syncMessage.viewOnceOpen) {
|
} else if (syncMessage.viewOnceOpen) {
|
||||||
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
|
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
|
||||||
|
} else if (syncMessage.messageRequestResponse) {
|
||||||
|
return this.handleMessageRequestResponse(
|
||||||
|
envelope,
|
||||||
|
syncMessage.messageRequestResponse
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
|
@ -1408,6 +1433,27 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
return this.dispatchAndWait(ev);
|
return this.dispatchAndWait(ev);
|
||||||
}
|
}
|
||||||
|
async handleMessageRequestResponse(
|
||||||
|
envelope: EnvelopeClass,
|
||||||
|
sync: SyncMessageClass.MessageRequestResponse
|
||||||
|
) {
|
||||||
|
window.log.info('got message request response sync message');
|
||||||
|
|
||||||
|
const ev = new Event('messageRequestResponse');
|
||||||
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
|
ev.threadE164 = sync.threadE164;
|
||||||
|
ev.threadUuid = sync.threadUuid;
|
||||||
|
ev.groupId = sync.groupId ? sync.groupId.toString('binary') : null;
|
||||||
|
ev.messageRequestResponseType = sync.type;
|
||||||
|
|
||||||
|
window.normalizeUuids(
|
||||||
|
ev,
|
||||||
|
['threadUuid'],
|
||||||
|
'MessageReceiver::handleMessageRequestResponse'
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.dispatchAndWait(ev);
|
||||||
|
}
|
||||||
async handleStickerPackOperation(
|
async handleStickerPackOperation(
|
||||||
envelope: EnvelopeClass,
|
envelope: EnvelopeClass,
|
||||||
operations: Array<SyncMessageClass.StickerPackOperation>
|
operations: Array<SyncMessageClass.StickerPackOperation>
|
||||||
|
|
|
@ -74,7 +74,7 @@ type MessageOptionsType = {
|
||||||
};
|
};
|
||||||
needsSync?: boolean;
|
needsSync?: boolean;
|
||||||
preview?: Array<PreviewType> | null;
|
preview?: Array<PreviewType> | null;
|
||||||
profileKey?: string;
|
profileKey?: ArrayBuffer;
|
||||||
quote?: any;
|
quote?: any;
|
||||||
recipients: Array<string>;
|
recipients: Array<string>;
|
||||||
sticker?: any;
|
sticker?: any;
|
||||||
|
@ -93,7 +93,7 @@ class Message {
|
||||||
};
|
};
|
||||||
needsSync?: boolean;
|
needsSync?: boolean;
|
||||||
preview: any;
|
preview: any;
|
||||||
profileKey?: string;
|
profileKey?: ArrayBuffer;
|
||||||
quote?: any;
|
quote?: any;
|
||||||
recipients: Array<string>;
|
recipients: Array<string>;
|
||||||
sticker?: any;
|
sticker?: any;
|
||||||
|
@ -274,6 +274,8 @@ export type AttachmentType = {
|
||||||
caption: string;
|
caption: string;
|
||||||
|
|
||||||
attachmentPointer?: AttachmentPointerClass;
|
attachmentPointer?: AttachmentPointerClass;
|
||||||
|
|
||||||
|
blurHash?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class MessageSender {
|
export default class MessageSender {
|
||||||
|
@ -348,6 +350,9 @@ export default class MessageSender {
|
||||||
if (attachment.caption) {
|
if (attachment.caption) {
|
||||||
proto.caption = attachment.caption;
|
proto.caption = attachment.caption;
|
||||||
}
|
}
|
||||||
|
if (attachment.blurHash) {
|
||||||
|
proto.blurHash = attachment.blurHash;
|
||||||
|
}
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
@ -862,6 +867,31 @@ export default class MessageSender {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendProfileKeyUpdate(
|
||||||
|
profileKey: ArrayBuffer,
|
||||||
|
recipients: Array<string>,
|
||||||
|
sendOptions: SendOptionsType,
|
||||||
|
groupId?: string
|
||||||
|
) {
|
||||||
|
return this.sendMessage(
|
||||||
|
{
|
||||||
|
recipients,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
profileKey,
|
||||||
|
flags: window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE,
|
||||||
|
...(groupId
|
||||||
|
? {
|
||||||
|
group: {
|
||||||
|
id: groupId,
|
||||||
|
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
sendOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async sendDeliveryReceipt(
|
async sendDeliveryReceipt(
|
||||||
recipientE164: string,
|
recipientE164: string,
|
||||||
recipientUuid: string,
|
recipientUuid: string,
|
||||||
|
@ -985,6 +1015,48 @@ export default class MessageSender {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncMessageRequestResponse(
|
||||||
|
responseArgs: {
|
||||||
|
threadE164?: string;
|
||||||
|
threadUuid?: string;
|
||||||
|
groupId?: string;
|
||||||
|
type: number;
|
||||||
|
},
|
||||||
|
sendOptions?: SendOptionsType
|
||||||
|
) {
|
||||||
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
|
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||||
|
if (myDevice === 1 || myDevice === '1') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncMessage = this.createSyncMessage();
|
||||||
|
|
||||||
|
const response = new window.textsecure.protobuf.SyncMessage.MessageRequestResponse();
|
||||||
|
response.threadE164 = responseArgs.threadE164;
|
||||||
|
response.threadUuid = responseArgs.threadUuid;
|
||||||
|
response.groupId = responseArgs.groupId
|
||||||
|
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(
|
||||||
|
responseArgs.groupId
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
response.type = responseArgs.type;
|
||||||
|
syncMessage.messageRequestResponse = response;
|
||||||
|
|
||||||
|
const contentMessage = new window.textsecure.protobuf.Content();
|
||||||
|
contentMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
|
const silent = true;
|
||||||
|
return this.sendIndividualProto(
|
||||||
|
myUuid || myNumber,
|
||||||
|
contentMessage,
|
||||||
|
Date.now(),
|
||||||
|
silent,
|
||||||
|
sendOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async sendStickerPackSync(
|
async sendStickerPackSync(
|
||||||
operations: Array<{
|
operations: Array<{
|
||||||
packId: string;
|
packId: string;
|
||||||
|
@ -1152,7 +1224,7 @@ export default class MessageSender {
|
||||||
reaction: any,
|
reaction: any,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
profileKey?: string,
|
profileKey?: ArrayBuffer,
|
||||||
flags?: number
|
flags?: number
|
||||||
) {
|
) {
|
||||||
const attributes = {
|
const attributes = {
|
||||||
|
@ -1195,7 +1267,7 @@ export default class MessageSender {
|
||||||
reaction: any,
|
reaction: any,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
profileKey?: string,
|
profileKey?: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
) {
|
||||||
return this.sendMessage(
|
return this.sendMessage(
|
||||||
|
@ -1308,7 +1380,7 @@ export default class MessageSender {
|
||||||
reaction: any,
|
reaction: any,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
profileKey?: string,
|
profileKey?: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
) {
|
||||||
const myE164 = window.textsecure.storage.user.getNumber();
|
const myE164 = window.textsecure.storage.user.getNumber();
|
||||||
|
@ -1480,7 +1552,7 @@ export default class MessageSender {
|
||||||
groupIdentifiers: Array<string>,
|
groupIdentifiers: Array<string>,
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
profileKey?: string,
|
profileKey?: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
) {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
|
@ -1517,7 +1589,7 @@ export default class MessageSender {
|
||||||
identifier: string,
|
identifier: string,
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
profileKey?: string,
|
profileKey?: ArrayBuffer,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
) {
|
||||||
return this.sendMessage(
|
return this.sendMessage(
|
||||||
|
|
|
@ -489,6 +489,7 @@ const URL_CALLS = {
|
||||||
signed: 'v2/keys/signed',
|
signed: 'v2/keys/signed',
|
||||||
getStickerPackUpload: 'v1/sticker/pack/form',
|
getStickerPackUpload: 'v1/sticker/pack/form',
|
||||||
whoami: 'v1/accounts/whoami',
|
whoami: 'v1/accounts/whoami',
|
||||||
|
config: 'v1/config',
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitializeOptionsType = {
|
type InitializeOptionsType = {
|
||||||
|
@ -604,6 +605,7 @@ export type WebAPIType = {
|
||||||
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
||||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||||
whoami: () => Promise<any>;
|
whoami: () => Promise<any>;
|
||||||
|
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignedPreKeyType = {
|
export type SignedPreKeyType = {
|
||||||
|
@ -724,9 +726,10 @@ export function initialize({
|
||||||
setSignedPreKey,
|
setSignedPreKey,
|
||||||
updateDeviceName,
|
updateDeviceName,
|
||||||
whoami,
|
whoami,
|
||||||
|
getConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function _ajax(param: AjaxOptionsType) {
|
async function _ajax(param: AjaxOptionsType): Promise<any> {
|
||||||
if (!param.urlParameters) {
|
if (!param.urlParameters) {
|
||||||
param.urlParameters = '';
|
param.urlParameters = '';
|
||||||
}
|
}
|
||||||
|
@ -792,6 +795,21 @@ export function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
type ResType = {
|
||||||
|
config: Array<{ name: string; enabled: boolean }>;
|
||||||
|
};
|
||||||
|
const res: ResType = await _ajax({
|
||||||
|
call: 'config',
|
||||||
|
httpType: 'GET',
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.config.filter(({ name }: { name: string }) =>
|
||||||
|
name.startsWith('desktop.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function getSenderCertificate() {
|
async function getSenderCertificate() {
|
||||||
return _ajax({
|
return _ajax({
|
||||||
call: 'deliveryCert',
|
call: 'deliveryCert',
|
||||||
|
|
|
@ -18,6 +18,7 @@ const MIN_HEIGHT = 50;
|
||||||
// Used for display
|
// Used for display
|
||||||
|
|
||||||
export interface AttachmentType {
|
export interface AttachmentType {
|
||||||
|
blurHash?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
@ -133,7 +134,7 @@ export function hasImage(attachments?: Array<AttachmentType>) {
|
||||||
return (
|
return (
|
||||||
attachments &&
|
attachments &&
|
||||||
attachments[0] &&
|
attachments[0] &&
|
||||||
(attachments[0].url || attachments[0].pending)
|
(attachments[0].url || attachments[0].pending || attachments[0].blurHash)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
50
ts/util/imageToBlurHash.ts
Normal file
50
ts/util/imageToBlurHash.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import loadImage from 'blueimp-load-image';
|
||||||
|
import { encode } from 'blurhash';
|
||||||
|
|
||||||
|
type Input = Parameters<typeof loadImage>[0];
|
||||||
|
|
||||||
|
const loadImageData = async (input: Input): Promise<ImageData> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
loadImage(
|
||||||
|
input,
|
||||||
|
canvasOrError => {
|
||||||
|
if (canvasOrError instanceof Event && canvasOrError.type === 'error') {
|
||||||
|
const processError = new Error(
|
||||||
|
'imageToBlurHash: Failed to process image'
|
||||||
|
);
|
||||||
|
processError.cause = canvasOrError;
|
||||||
|
reject(processError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canvasOrError instanceof HTMLCanvasElement) {
|
||||||
|
const context = canvasOrError.getContext('2d');
|
||||||
|
resolve(
|
||||||
|
context?.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvasOrError.width,
|
||||||
|
canvasOrError.height
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const error = new Error(
|
||||||
|
'imageToBlurHash: Failed to place image on canvas'
|
||||||
|
);
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
// Calculating the blurhash on large images is a long-running and
|
||||||
|
// synchronous operation, so here we ensure the images are a reasonable
|
||||||
|
// size before calculating the blurhash. iOS uses a max size of 200x200
|
||||||
|
// and Android uses a max size of 1/16 the original size. 200x200 is
|
||||||
|
// easier for us.
|
||||||
|
{ canvas: true, orientation: true, maxWidth: 200, maxHeight: 200 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const imageToBlurHash = async (input: Input) => {
|
||||||
|
const { data, width, height } = await loadImageData(input);
|
||||||
|
// 4 horizontal components and 3 vertical components
|
||||||
|
return encode(data, width, height, 4, 3);
|
||||||
|
};
|
|
@ -219,6 +219,14 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-03-25T15:45:04.024Z"
|
"updated": "2020-03-25T15:45:04.024Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "js/models/conversations.js",
|
||||||
|
"line": " await wrap(",
|
||||||
|
"lineNumber": 641,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-05-27T21:15:43.044Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/modules/debuglogs.js",
|
"path": "js/modules/debuglogs.js",
|
||||||
|
@ -570,7 +578,7 @@
|
||||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||||
"lineNumber": 198,
|
"lineNumber": 198,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-10-21T22:30:15.622Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -579,7 +587,7 @@
|
||||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||||
"lineNumber": 201,
|
"lineNumber": 201,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-10-21T22:30:15.622Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11426,18 +11434,18 @@
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "ts/components/CompositionArea.js",
|
"path": "ts/components/CompositionArea.js",
|
||||||
"line": " el.innerHTML = '';",
|
"line": " el.innerHTML = '';",
|
||||||
"lineNumber": 22,
|
"lineNumber": 23,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-08-01T14:10:37.481Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "ts/components/CompositionArea.tsx",
|
"path": "ts/components/CompositionArea.tsx",
|
||||||
"line": " el.innerHTML = '';",
|
"line": " el.innerHTML = '';",
|
||||||
"lineNumber": 73,
|
"lineNumber": 80,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-12-16T14:36:25.614ZZ",
|
"updated": "2020-06-03T19:23:21.195Z",
|
||||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11507,9 +11515,9 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||||
"line": " this.menuTriggerRef = React.createRef();",
|
"line": " this.menuTriggerRef = React.createRef();",
|
||||||
"lineNumber": 67,
|
"lineNumber": 68,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-07-31T00:19:18.696Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Used to reference popup menu"
|
"reasonDetail": "Used to reference popup menu"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11553,7 +11561,7 @@
|
||||||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||||
"lineNumber": 184,
|
"lineNumber": 184,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-04-30T15:59:13.160Z"
|
"updated": "2020-05-21T16:56:07.875Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
@ -11561,7 +11569,7 @@
|
||||||
"line": " > = React.createRef();",
|
"line": " > = React.createRef();",
|
||||||
"lineNumber": 188,
|
"lineNumber": 188,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-04-30T15:59:13.160Z"
|
"updated": "2020-05-21T16:56:07.875Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
@ -11784,4 +11792,4 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-04-05T23:45:16.746Z"
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
6
ts/window.d.ts
vendored
6
ts/window.d.ts
vendored
|
@ -31,7 +31,7 @@ declare global {
|
||||||
storage: {
|
storage: {
|
||||||
put: (key: string, value: any) => void;
|
put: (key: string, value: any) => void;
|
||||||
remove: (key: string) => void;
|
remove: (key: string) => void;
|
||||||
get: (key: string) => any;
|
get: <T = any>(key: string) => T | undefined;
|
||||||
};
|
};
|
||||||
textsecure: TextSecureType;
|
textsecure: TextSecureType;
|
||||||
|
|
||||||
|
@ -48,6 +48,10 @@ declare global {
|
||||||
WebAPI: WebAPIConnectType;
|
WebAPI: WebAPIConnectType;
|
||||||
Whisper: WhisperType;
|
Whisper: WhisperType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Error {
|
||||||
|
cause?: Event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConversationType = {
|
export type ConversationType = {
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -2052,6 +2052,13 @@
|
||||||
"@types/jquery" "*"
|
"@types/jquery" "*"
|
||||||
"@types/underscore" "*"
|
"@types/underscore" "*"
|
||||||
|
|
||||||
|
"@types/blueimp-load-image@2.23.6":
|
||||||
|
version "2.23.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/blueimp-load-image/-/blueimp-load-image-2.23.6.tgz#2ccf3c69bd17c5bd1f4471470505a7f065a84a9f"
|
||||||
|
integrity sha512-RF5EQ2miGm/o5XmZk0PhpLnikXOwe1BxGJVikYcNhwr4s7i7LqVc4U51k/zFTD11GeqyK+yz6vWwvvtoKsPYLw==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.17.1"
|
version "1.17.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897"
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897"
|
||||||
|
@ -3957,6 +3964,11 @@ blueimp-load-image@2.18.0:
|
||||||
resolved "https://registry.yarnpkg.com/blueimp-load-image/-/blueimp-load-image-2.18.0.tgz#03b93687eb382a7136cfbcbd4f0e936b6763fc0e"
|
resolved "https://registry.yarnpkg.com/blueimp-load-image/-/blueimp-load-image-2.18.0.tgz#03b93687eb382a7136cfbcbd4f0e936b6763fc0e"
|
||||||
integrity sha512-GUrxVE/7FpzAw/WU6GMiI3v+LpFmlAxp7sF36EQB8rGAg97ND8iTeYZ3FQbhsxS5s2dNarGKZEWhKPNKKSmMuA==
|
integrity sha512-GUrxVE/7FpzAw/WU6GMiI3v+LpFmlAxp7sF36EQB8rGAg97ND8iTeYZ3FQbhsxS5s2dNarGKZEWhKPNKKSmMuA==
|
||||||
|
|
||||||
|
blurhash@1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||||
|
@ -13579,6 +13591,11 @@ rc@^1.1.2:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
react-blurhash@0.1.2:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-blurhash/-/react-blurhash-0.1.2.tgz#16bdce59be4f48dc7816a26e8f0435f73d3a2bb2"
|
||||||
|
integrity sha512-p7TAQ+Qw78rBO9LSxkURfNDJA8TGAuOW2GVJKzNHrOY61XtA02PYmpAY9lotbFpftczEXZ7h4Su2JRmEUI7/Hw==
|
||||||
|
|
||||||
react-clientside-effect@^1.2.0:
|
react-clientside-effect@^1.2.0:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
|
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
|
||||||
|
|
Loading…
Reference in a new issue