Move left pane entirely to React
This commit is contained in:
parent
bf904ddd12
commit
b3ac1373fa
142 changed files with 5016 additions and 3428 deletions
|
@ -177,6 +177,12 @@
|
|||
const PASSWORD = storage.get('password');
|
||||
accountManager = new textsecure.AccountManager(USERNAME, PASSWORD);
|
||||
accountManager.addEventListener('registration', () => {
|
||||
const user = {
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourNumber: textsecure.storage.user.getNumber(),
|
||||
};
|
||||
Whisper.events.trigger('userChanged', user);
|
||||
|
||||
Whisper.Registration.markDone();
|
||||
window.log.info('dispatching registration event');
|
||||
Whisper.events.trigger('registration_done');
|
||||
|
@ -535,16 +541,16 @@
|
|||
window.addEventListener('focus', () => Whisper.Notifications.clear());
|
||||
window.addEventListener('unload', () => Whisper.Notifications.fastClear());
|
||||
|
||||
Whisper.events.on('showConversation', conversation => {
|
||||
Whisper.events.on('showConversation', (id, messageId) => {
|
||||
if (appView) {
|
||||
appView.openConversation(conversation);
|
||||
appView.openConversation(id, messageId);
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.Notifications.on('click', conversation => {
|
||||
Whisper.Notifications.on('click', (id, messageId) => {
|
||||
window.showWindow();
|
||||
if (conversation) {
|
||||
appView.openConversation(conversation);
|
||||
if (id) {
|
||||
appView.openConversation(id, messageId);
|
||||
} else {
|
||||
appView.openInbox({
|
||||
initialLoadComplete,
|
||||
|
|
|
@ -21,25 +21,6 @@
|
|||
_.debounce(this.updateUnreadCount.bind(this), 1000)
|
||||
);
|
||||
this.startPruning();
|
||||
|
||||
this.collator = new Intl.Collator();
|
||||
},
|
||||
comparator(m1, m2) {
|
||||
const timestamp1 = m1.get('timestamp');
|
||||
const timestamp2 = m2.get('timestamp');
|
||||
if (timestamp1 && !timestamp2) {
|
||||
return -1;
|
||||
}
|
||||
if (timestamp2 && !timestamp1) {
|
||||
return 1;
|
||||
}
|
||||
if (timestamp1 && timestamp2 && timestamp1 !== timestamp2) {
|
||||
return timestamp2 - timestamp1;
|
||||
}
|
||||
|
||||
const title1 = m1.getTitle().toLowerCase();
|
||||
const title2 = m2.getTitle().toLowerCase();
|
||||
return this.collator.compare(title1, title2);
|
||||
},
|
||||
addActive(model) {
|
||||
if (model.get('active_at')) {
|
||||
|
@ -78,18 +59,6 @@
|
|||
window.getInboxCollection = () => inboxCollection;
|
||||
|
||||
window.ConversationController = {
|
||||
markAsSelected(toSelect) {
|
||||
conversations.each(conversation => {
|
||||
const current = conversation.isSelected || false;
|
||||
const newValue = conversation.id === toSelect.id;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
conversation.isSelected = newValue;
|
||||
if (current !== newValue) {
|
||||
conversation.trigger('change');
|
||||
}
|
||||
});
|
||||
},
|
||||
get(id) {
|
||||
if (!this._initialFetchComplete) {
|
||||
throw new Error(
|
||||
|
|
|
@ -29,6 +29,12 @@
|
|||
Message: Whisper.Message,
|
||||
});
|
||||
|
||||
Whisper.events.trigger(
|
||||
'messageExpired',
|
||||
message.id,
|
||||
message.conversationId
|
||||
);
|
||||
|
||||
const conversation = message.getConversation();
|
||||
if (conversation) {
|
||||
conversation.trigger('expired', message);
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
/* global _: false */
|
||||
/* global Backbone: false */
|
||||
/* global libphonenumber: false */
|
||||
|
||||
/* global ConversationController: false */
|
||||
/* global libsignal: false */
|
||||
/* global storage: false */
|
||||
/* global textsecure: false */
|
||||
/* global Whisper: false */
|
||||
/* global
|
||||
_,
|
||||
i18n,
|
||||
Backbone,
|
||||
libphonenumber,
|
||||
ConversationController,
|
||||
libsignal,
|
||||
storage,
|
||||
textsecure,
|
||||
Whisper
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
|
@ -138,6 +140,13 @@
|
|||
|
||||
this.typingRefreshTimer = null;
|
||||
this.typingPauseTimer = null;
|
||||
|
||||
// Keep props ready
|
||||
const generateProps = () => {
|
||||
this.cachedProps = this.getProps();
|
||||
};
|
||||
this.on('change', generateProps);
|
||||
generateProps();
|
||||
},
|
||||
|
||||
isMe() {
|
||||
|
@ -292,40 +301,37 @@
|
|||
},
|
||||
|
||||
format() {
|
||||
return this.cachedProps;
|
||||
},
|
||||
getProps() {
|
||||
const { format } = PhoneNumber;
|
||||
const regionCode = storage.get('regionCode');
|
||||
const color = this.getColor();
|
||||
|
||||
return {
|
||||
phoneNumber: format(this.id, {
|
||||
ourRegionCode: regionCode,
|
||||
}),
|
||||
color,
|
||||
avatarPath: this.getAvatarPath(),
|
||||
name: this.getName(),
|
||||
profileName: this.getProfileName(),
|
||||
title: this.getTitle(),
|
||||
};
|
||||
},
|
||||
getPropsForListItem() {
|
||||
const typingKeys = Object.keys(this.contactTypingTimers || {});
|
||||
|
||||
const result = {
|
||||
...this.format(),
|
||||
id: this.id,
|
||||
|
||||
activeAt: this.get('active_at'),
|
||||
avatarPath: this.getAvatarPath(),
|
||||
color,
|
||||
type: this.isPrivate() ? 'direct' : 'group',
|
||||
isMe: this.isMe(),
|
||||
conversationType: this.isPrivate() ? 'direct' : 'group',
|
||||
|
||||
lastUpdated: this.get('timestamp'),
|
||||
unreadCount: this.get('unreadCount') || 0,
|
||||
isSelected: this.isSelected,
|
||||
|
||||
isTyping: typingKeys.length > 0,
|
||||
lastUpdated: this.get('timestamp'),
|
||||
name: this.getName(),
|
||||
profileName: this.getProfileName(),
|
||||
timestamp: this.get('timestamp'),
|
||||
title: this.getTitle(),
|
||||
unreadCount: this.get('unreadCount') || 0,
|
||||
|
||||
phoneNumber: format(this.id, {
|
||||
ourRegionCode: regionCode,
|
||||
}),
|
||||
lastMessage: {
|
||||
status: this.get('lastMessageStatus'),
|
||||
text: this.get('lastMessage'),
|
||||
},
|
||||
|
||||
onClick: () => this.trigger('select', this),
|
||||
};
|
||||
|
||||
return result;
|
||||
|
@ -572,8 +578,8 @@
|
|||
onMemberVerifiedChange() {
|
||||
// If the verified state of a member changes, our aggregate state changes.
|
||||
// We trigger both events to replicate the behavior of Backbone.Model.set()
|
||||
this.trigger('change:verified');
|
||||
this.trigger('change');
|
||||
this.trigger('change:verified', this);
|
||||
this.trigger('change', this);
|
||||
},
|
||||
toggleVerified() {
|
||||
if (this.isVerified()) {
|
||||
|
@ -1798,7 +1804,7 @@
|
|||
if (this.isPrivate()) {
|
||||
return this.get('name');
|
||||
}
|
||||
return this.get('name') || 'Unknown group';
|
||||
return this.get('name') || i18n('unknownGroup');
|
||||
},
|
||||
|
||||
getTitle() {
|
||||
|
@ -1990,14 +1996,14 @@
|
|||
if (!record) {
|
||||
// User was not previously typing before. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change');
|
||||
this.trigger('change', this);
|
||||
}
|
||||
} else {
|
||||
delete this.contactTypingTimers[identifier];
|
||||
if (record) {
|
||||
// User was previously typing, and is no longer. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change');
|
||||
this.trigger('change', this);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2012,7 +2018,7 @@
|
|||
|
||||
// User was previously typing, but timed out or we received message. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change');
|
||||
this.trigger('change', this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -2034,21 +2040,6 @@
|
|||
);
|
||||
this.reset([]);
|
||||
},
|
||||
|
||||
async search(providedQuery) {
|
||||
let query = providedQuery.trim().toLowerCase();
|
||||
query = query.replace(/[+-.()]*/g, '');
|
||||
|
||||
if (query.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = await window.Signal.Data.searchConversations(query, {
|
||||
ConversationCollection: Whisper.ConversationCollection,
|
||||
});
|
||||
|
||||
this.reset(collection.models);
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
||||
|
|
|
@ -83,6 +83,42 @@
|
|||
this.on('unload', this.unload);
|
||||
this.on('expired', this.onExpired);
|
||||
this.setToExpire();
|
||||
|
||||
// Keep props ready
|
||||
const generateProps = () => {
|
||||
if (this.isExpirationTimerUpdate()) {
|
||||
this.propsForTimerNotification = this.getPropsForTimerNotification();
|
||||
} else if (this.isKeyChange()) {
|
||||
this.propsForSafetyNumberNotification = this.getPropsForSafetyNumberNotification();
|
||||
} else if (this.isVerifiedChange()) {
|
||||
this.propsForVerificationNotification = this.getPropsForVerificationNotification();
|
||||
} else if (this.isEndSession()) {
|
||||
this.propsForResetSessionNotification = this.getPropsForResetSessionNotification();
|
||||
} else if (this.isGroupUpdate()) {
|
||||
this.propsForGroupNotification = this.getPropsForGroupNotification();
|
||||
} else {
|
||||
this.propsForSearchResult = this.getPropsForSearchResult();
|
||||
this.propsForMessage = this.getPropsForMessage();
|
||||
}
|
||||
};
|
||||
this.on('change', generateProps);
|
||||
|
||||
const applicableConversationChanges =
|
||||
'change:color change:name change:number change:profileName change:profileAvatar';
|
||||
|
||||
const conversation = this.getConversation();
|
||||
const fromContact = this.getIncomingContact();
|
||||
|
||||
this.listenTo(conversation, applicableConversationChanges, generateProps);
|
||||
if (fromContact) {
|
||||
this.listenTo(
|
||||
fromContact,
|
||||
applicableConversationChanges,
|
||||
generateProps
|
||||
);
|
||||
}
|
||||
|
||||
generateProps();
|
||||
},
|
||||
idForLogging() {
|
||||
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
|
||||
|
@ -387,6 +423,35 @@
|
|||
|
||||
return 'sending';
|
||||
},
|
||||
getPropsForSearchResult() {
|
||||
const fromNumber = this.getSource();
|
||||
const from = this.findAndFormatContact(fromNumber);
|
||||
if (fromNumber === this.OUR_NUMBER) {
|
||||
from.isMe = true;
|
||||
}
|
||||
|
||||
const toNumber = this.get('conversationId');
|
||||
let to = this.findAndFormatContact(toNumber);
|
||||
if (toNumber === this.OUR_NUMBER) {
|
||||
to.isMe = true;
|
||||
} else if (fromNumber === toNumber) {
|
||||
to = {
|
||||
isMe: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
|
||||
isSelected: this.isSelected,
|
||||
|
||||
id: this.id,
|
||||
conversationId: this.get('conversationId'),
|
||||
receivedAt: this.get('received_at'),
|
||||
snippet: this.get('snippet'),
|
||||
};
|
||||
},
|
||||
getPropsForMessage() {
|
||||
const phoneNumber = this.getSource();
|
||||
const contact = this.findAndFormatContact(phoneNumber);
|
||||
|
@ -495,7 +560,7 @@
|
|||
// Would be nice to do this before render, on initial load of message
|
||||
if (!window.isSignalAccountCheckComplete(firstNumber)) {
|
||||
window.checkForSignalAccount(firstNumber).then(() => {
|
||||
this.trigger('change');
|
||||
this.trigger('change', this);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -282,9 +282,9 @@ async function _finishJob(message, id) {
|
|||
|
||||
if (fromConversation && message !== fromConversation) {
|
||||
fromConversation.set(message.attributes);
|
||||
fromConversation.trigger('change');
|
||||
fromConversation.trigger('change', fromConversation);
|
||||
} else {
|
||||
message.trigger('change');
|
||||
message.trigger('change', message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
js/modules/data.d.ts
vendored
Normal file
2
js/modules/data.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
export function searchMessages(query: string): Promise<Array<any>>;
|
||||
export function searchConversations(query: string): Promise<Array<any>>;
|
|
@ -96,7 +96,10 @@ module.exports = {
|
|||
getAllConversationIds,
|
||||
getAllPrivateConversations,
|
||||
getAllGroupsInvolvingId,
|
||||
|
||||
searchConversations,
|
||||
searchMessages,
|
||||
searchMessagesInConversation,
|
||||
|
||||
getMessageCount,
|
||||
saveMessage,
|
||||
|
@ -624,12 +627,27 @@ async function getAllGroupsInvolvingId(id, { ConversationCollection }) {
|
|||
return collection;
|
||||
}
|
||||
|
||||
async function searchConversations(query, { ConversationCollection }) {
|
||||
async function searchConversations(query) {
|
||||
const conversations = await channels.searchConversations(query);
|
||||
return conversations;
|
||||
}
|
||||
|
||||
const collection = new ConversationCollection();
|
||||
collection.add(conversations);
|
||||
return collection;
|
||||
async function searchMessages(query, { limit } = {}) {
|
||||
const messages = await channels.searchMessages(query, { limit });
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function searchMessagesInConversation(
|
||||
query,
|
||||
conversationId,
|
||||
{ limit } = {}
|
||||
) {
|
||||
const messages = await channels.searchMessagesInConversation(
|
||||
query,
|
||||
conversationId,
|
||||
{ limit }
|
||||
);
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Message
|
||||
|
|
|
@ -329,8 +329,11 @@ function isChunkSneaky(chunk) {
|
|||
function isLinkSneaky(link) {
|
||||
const domain = getDomain(link);
|
||||
|
||||
// This is necesary because getDomain returns domains in punycode form
|
||||
const unicodeDomain = nodeUrl.domainToUnicode(domain);
|
||||
// This is necesary because getDomain returns domains in punycode form. We check whether
|
||||
// it's available for the StyleGuide.
|
||||
const unicodeDomain = nodeUrl.domainToUnicode
|
||||
? nodeUrl.domainToUnicode(domain)
|
||||
: domain;
|
||||
|
||||
const chunks = unicodeDomain.split('.');
|
||||
for (let i = 0, max = chunks.length; i < max; i += 1) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// The idea with this file is to make it webpackable for the style guide
|
||||
|
||||
const { bindActionCreators } = require('redux');
|
||||
const Backbone = require('../../ts/backbone');
|
||||
const Crypto = require('./crypto');
|
||||
const Data = require('./data');
|
||||
|
@ -29,9 +30,6 @@ const { ContactName } = require('../../ts/components/conversation/ContactName');
|
|||
const {
|
||||
ConversationHeader,
|
||||
} = require('../../ts/components/conversation/ConversationHeader');
|
||||
const {
|
||||
ConversationListItem,
|
||||
} = require('../../ts/components/ConversationListItem');
|
||||
const {
|
||||
EmbeddedContact,
|
||||
} = require('../../ts/components/conversation/EmbeddedContact');
|
||||
|
@ -44,7 +42,6 @@ const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
|||
const {
|
||||
MediaGallery,
|
||||
} = require('../../ts/components/conversation/media-gallery/MediaGallery');
|
||||
const { MainHeader } = require('../../ts/components/MainHeader');
|
||||
const { Message } = require('../../ts/components/conversation/Message');
|
||||
const { MessageBody } = require('../../ts/components/conversation/MessageBody');
|
||||
const {
|
||||
|
@ -70,6 +67,12 @@ const {
|
|||
VerificationNotification,
|
||||
} = require('../../ts/components/conversation/VerificationNotification');
|
||||
|
||||
// State
|
||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||
const { createStore } = require('../../ts/state/createStore');
|
||||
const conversationsDuck = require('../../ts/state/ducks/conversations');
|
||||
const userDuck = require('../../ts/state/ducks/user');
|
||||
|
||||
// Migrations
|
||||
const {
|
||||
getPlaceholderMigrations,
|
||||
|
@ -201,13 +204,11 @@ exports.setup = (options = {}) => {
|
|||
ContactListItem,
|
||||
ContactName,
|
||||
ConversationHeader,
|
||||
ConversationListItem,
|
||||
EmbeddedContact,
|
||||
Emojify,
|
||||
GroupNotification,
|
||||
Lightbox,
|
||||
LightboxGallery,
|
||||
MainHeader,
|
||||
MediaGallery,
|
||||
Message,
|
||||
MessageBody,
|
||||
|
@ -224,6 +225,20 @@ exports.setup = (options = {}) => {
|
|||
VerificationNotification,
|
||||
};
|
||||
|
||||
const Roots = {
|
||||
createLeftPane,
|
||||
};
|
||||
const Ducks = {
|
||||
conversations: conversationsDuck,
|
||||
user: userDuck,
|
||||
};
|
||||
const State = {
|
||||
bindActionCreators,
|
||||
createStore,
|
||||
Roots,
|
||||
Ducks,
|
||||
};
|
||||
|
||||
const Types = {
|
||||
Attachment: AttachmentType,
|
||||
Contact,
|
||||
|
@ -262,6 +277,7 @@ exports.setup = (options = {}) => {
|
|||
OS,
|
||||
RefreshSenderCertificate,
|
||||
Settings,
|
||||
State,
|
||||
Types,
|
||||
Util,
|
||||
Views,
|
||||
|
|
|
@ -20,7 +20,7 @@ const {
|
|||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// digest: ArrayBuffer
|
||||
// fileName: string | null
|
||||
// fileName?: string
|
||||
// flags: null
|
||||
// key: ArrayBuffer
|
||||
// size: integer
|
||||
|
|
|
@ -39,10 +39,6 @@
|
|||
this.fastUpdate = this.update;
|
||||
this.update = _.debounce(this.update, 1000);
|
||||
},
|
||||
onClick(conversationId) {
|
||||
const conversation = ConversationController.get(conversationId);
|
||||
this.trigger('click', conversation);
|
||||
},
|
||||
update() {
|
||||
if (this.lastNotification) {
|
||||
this.lastNotification.close();
|
||||
|
@ -148,7 +144,8 @@
|
|||
tag: isNotificationGroupingSupported ? 'signal' : undefined,
|
||||
silent: !status.shouldPlayNotificationSound,
|
||||
});
|
||||
notification.onclick = () => this.onClick(last.conversationId);
|
||||
notification.onclick = () =>
|
||||
this.trigger('click', last.conversationId, last.id);
|
||||
this.lastNotification = notification;
|
||||
|
||||
// We continue to build up more and more messages for our notifications
|
||||
|
|
|
@ -171,10 +171,10 @@
|
|||
view.onProgress(count);
|
||||
}
|
||||
},
|
||||
openConversation(conversation) {
|
||||
if (conversation) {
|
||||
openConversation(id, messageId) {
|
||||
if (id) {
|
||||
this.openInbox().then(() => {
|
||||
this.inboxView.openConversation(conversation);
|
||||
this.inboxView.openConversation(id, messageId);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,230 +0,0 @@
|
|||
/* global $: false */
|
||||
/* global _: false */
|
||||
/* global Backbone: false */
|
||||
/* global filesize: false */
|
||||
|
||||
/* global i18n: false */
|
||||
/* global Signal: false */
|
||||
/* global Whisper: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const FileView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'fileView',
|
||||
templateName: 'file-view',
|
||||
render_attributes() {
|
||||
return this.model;
|
||||
},
|
||||
});
|
||||
|
||||
const ImageView = Backbone.View.extend({
|
||||
tagName: 'img',
|
||||
initialize(blobUrl) {
|
||||
this.blobUrl = blobUrl;
|
||||
},
|
||||
events: {
|
||||
load: 'update',
|
||||
},
|
||||
update() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render() {
|
||||
this.$el.attr('src', this.blobUrl);
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
const MediaView = Backbone.View.extend({
|
||||
initialize(dataUrl, { contentType } = {}) {
|
||||
this.dataUrl = dataUrl;
|
||||
this.contentType = contentType;
|
||||
this.$el.attr('controls', '');
|
||||
},
|
||||
events: {
|
||||
canplay: 'canplay',
|
||||
},
|
||||
canplay() {
|
||||
this.trigger('update');
|
||||
},
|
||||
render() {
|
||||
const $el = $('<source>');
|
||||
$el.attr('src', this.dataUrl);
|
||||
this.$el.append($el);
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
const AudioView = MediaView.extend({ tagName: 'audio' });
|
||||
const VideoView = MediaView.extend({ tagName: 'video' });
|
||||
|
||||
// Blacklist common file types known to be unsupported in Chrome
|
||||
const unsupportedFileTypes = ['audio/aiff', 'video/quicktime'];
|
||||
|
||||
Whisper.AttachmentView = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
className() {
|
||||
if (this.isImage()) {
|
||||
return 'attachment';
|
||||
}
|
||||
return 'attachment bubbled';
|
||||
},
|
||||
initialize(options) {
|
||||
this.blob = new Blob([this.model.data], { type: this.model.contentType });
|
||||
if (!this.model.size) {
|
||||
this.model.size = this.model.data.byteLength;
|
||||
}
|
||||
if (options.timestamp) {
|
||||
this.timestamp = options.timestamp;
|
||||
}
|
||||
},
|
||||
events: {
|
||||
click: 'onClick',
|
||||
},
|
||||
unload() {
|
||||
this.blob = null;
|
||||
|
||||
if (this.lightboxView) {
|
||||
this.lightboxView.remove();
|
||||
}
|
||||
if (this.fileView) {
|
||||
this.fileView.remove();
|
||||
}
|
||||
if (this.view) {
|
||||
this.view.remove();
|
||||
}
|
||||
|
||||
this.remove();
|
||||
},
|
||||
onClick() {
|
||||
if (!this.isImage()) {
|
||||
this.saveFile();
|
||||
return;
|
||||
}
|
||||
|
||||
const props = {
|
||||
objectURL: this.objectUrl,
|
||||
contentType: this.model.contentType,
|
||||
onSave: () => this.saveFile(),
|
||||
// implicit: `close`
|
||||
};
|
||||
this.lightboxView = new Whisper.ReactWrapperView({
|
||||
Component: Signal.Components.Lightbox,
|
||||
props,
|
||||
onClose: () => Signal.Backbone.Views.Lightbox.hide(),
|
||||
});
|
||||
Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
|
||||
},
|
||||
isVoiceMessage() {
|
||||
return Signal.Types.Attachment.isVoiceMessage(this.model);
|
||||
},
|
||||
isAudio() {
|
||||
const { contentType } = this.model;
|
||||
// TODO: Implement and use `Signal.Util.GoogleChrome.isAudioTypeSupported`:
|
||||
return Signal.Types.MIME.isAudio(contentType);
|
||||
},
|
||||
isVideo() {
|
||||
const { contentType } = this.model;
|
||||
return Signal.Util.GoogleChrome.isVideoTypeSupported(contentType);
|
||||
},
|
||||
isImage() {
|
||||
const { contentType } = this.model;
|
||||
return Signal.Util.GoogleChrome.isImageTypeSupported(contentType);
|
||||
},
|
||||
mediaType() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return 'voice';
|
||||
} else if (this.isAudio()) {
|
||||
return 'audio';
|
||||
} else if (this.isVideo()) {
|
||||
return 'video';
|
||||
} else if (this.isImage()) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
// NOTE: The existing code had no `return` but ESLint insists. Thought
|
||||
// about throwing an error assuming this was unreachable code but it turns
|
||||
// out that content type `image/tiff` falls through here:
|
||||
return undefined;
|
||||
},
|
||||
displayName() {
|
||||
if (this.isVoiceMessage()) {
|
||||
return i18n('voiceMessage');
|
||||
}
|
||||
if (this.model.fileName) {
|
||||
return this.model.fileName;
|
||||
}
|
||||
if (this.isAudio() || this.isVideo()) {
|
||||
return i18n('mediaMessage');
|
||||
}
|
||||
|
||||
return i18n('unnamedFile');
|
||||
},
|
||||
saveFile() {
|
||||
Signal.Types.Attachment.save({
|
||||
attachment: this.model,
|
||||
document,
|
||||
getAbsolutePath: Signal.Migrations.getAbsoluteAttachmentPath,
|
||||
timestamp: this.timestamp,
|
||||
});
|
||||
},
|
||||
render() {
|
||||
if (!this.isImage()) {
|
||||
this.renderFileView();
|
||||
}
|
||||
let View;
|
||||
if (this.isImage()) {
|
||||
View = ImageView;
|
||||
} else if (this.isAudio()) {
|
||||
View = AudioView;
|
||||
} else if (this.isVideo()) {
|
||||
View = VideoView;
|
||||
}
|
||||
|
||||
if (!View || _.contains(unsupportedFileTypes, this.model.contentType)) {
|
||||
this.update();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.objectUrl) {
|
||||
this.objectUrl = window.URL.createObjectURL(this.blob);
|
||||
}
|
||||
|
||||
const { blob } = this;
|
||||
const { contentType } = this.model;
|
||||
this.view = new View(this.objectUrl, { blob, contentType });
|
||||
this.view.$el.appendTo(this.$el);
|
||||
this.listenTo(this.view, 'update', this.update);
|
||||
this.view.render();
|
||||
if (View !== ImageView) {
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 5000);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
onTimeout() {
|
||||
// Image or media element failed to load. Fall back to FileView.
|
||||
this.stopListening(this.view);
|
||||
this.update();
|
||||
},
|
||||
renderFileView() {
|
||||
this.fileView = new FileView({
|
||||
model: {
|
||||
mediaType: this.mediaType(),
|
||||
fileName: this.displayName(),
|
||||
fileSize: filesize(this.model.size),
|
||||
altText: i18n('clickToSave'),
|
||||
},
|
||||
});
|
||||
|
||||
this.fileView.$el.appendTo(this.$el.empty());
|
||||
this.fileView.render();
|
||||
return this;
|
||||
},
|
||||
update() {
|
||||
clearTimeout(this.timeout);
|
||||
this.trigger('update');
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,51 +0,0 @@
|
|||
/* global Whisper, Signal, Backbone */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
// list of conversations, showing user/group and last message sent
|
||||
Whisper.ConversationListItemView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
className() {
|
||||
return `conversation-list-item contact ${this.model.cid}`;
|
||||
},
|
||||
templateName: 'conversation-preview',
|
||||
initialize() {
|
||||
this.listenTo(this.model, 'destroy', this.remove);
|
||||
},
|
||||
|
||||
remove() {
|
||||
if (this.childView) {
|
||||
this.childView.remove();
|
||||
this.childView = null;
|
||||
}
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.childView) {
|
||||
this.childView.remove();
|
||||
this.childView = null;
|
||||
}
|
||||
|
||||
const props = this.model.getPropsForListItem();
|
||||
this.childView = new Whisper.ReactWrapperView({
|
||||
className: 'list-item-wrapper',
|
||||
Component: Signal.Components.ConversationListItem,
|
||||
props,
|
||||
});
|
||||
|
||||
const update = () =>
|
||||
this.childView.update(this.model.getPropsForListItem());
|
||||
|
||||
this.listenTo(this.model, 'change', update);
|
||||
|
||||
this.$el.append(this.childView.el);
|
||||
|
||||
return this;
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,68 +0,0 @@
|
|||
/* global Whisper, getInboxCollection, $ */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.ConversationListView = Whisper.ListView.extend({
|
||||
tagName: 'div',
|
||||
itemView: Whisper.ConversationListItemView,
|
||||
updateLocation(conversation) {
|
||||
const $el = this.$(`.${conversation.cid}`);
|
||||
|
||||
if (!$el || !$el.length) {
|
||||
window.log.warn(
|
||||
'updateLocation: did not find element for conversation',
|
||||
conversation.idForLogging()
|
||||
);
|
||||
return;
|
||||
}
|
||||
if ($el.length > 1) {
|
||||
window.log.warn(
|
||||
'updateLocation: found more than one element for conversation',
|
||||
conversation.idForLogging()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const $allConversations = this.$('.conversation-list-item');
|
||||
const inboxCollection = getInboxCollection();
|
||||
const index = inboxCollection.indexOf(conversation);
|
||||
|
||||
const elIndex = $allConversations.index($el);
|
||||
if (elIndex < 0) {
|
||||
window.log.warn(
|
||||
'updateLocation: did not find index for conversation',
|
||||
conversation.idForLogging()
|
||||
);
|
||||
}
|
||||
|
||||
if (index === elIndex) {
|
||||
return;
|
||||
}
|
||||
if (index === 0) {
|
||||
this.$el.prepend($el);
|
||||
} else if (index === this.collection.length - 1) {
|
||||
this.$el.append($el);
|
||||
} else {
|
||||
const targetConversation = inboxCollection.at(index - 1);
|
||||
const target = this.$(`.${targetConversation.cid}`);
|
||||
$el.insertAfter(target);
|
||||
}
|
||||
|
||||
if ($('.selected').length) {
|
||||
$('.selected')[0].scrollIntoView({
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
},
|
||||
removeItem(conversation) {
|
||||
const $el = this.$(`.${conversation.cid}`);
|
||||
if ($el && $el.length > 0) {
|
||||
$el.remove();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,171 +0,0 @@
|
|||
/* global ConversationController, i18n, textsecure, Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const isSearchable = conversation => conversation.isSearchable();
|
||||
|
||||
Whisper.NewContactView = Whisper.View.extend({
|
||||
templateName: 'new-contact',
|
||||
className: 'conversation-list-item contact',
|
||||
events: {
|
||||
click: 'validate',
|
||||
},
|
||||
initialize() {
|
||||
this.listenTo(this.model, 'change', this.render); // auto update
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
number: i18n('startConversation'),
|
||||
title: this.model.getNumber(),
|
||||
avatar: this.model.getAvatar(),
|
||||
};
|
||||
},
|
||||
validate() {
|
||||
if (this.model.isValid()) {
|
||||
this.$el.addClass('valid');
|
||||
} else {
|
||||
this.$el.removeClass('valid');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.ConversationSearchView = Whisper.View.extend({
|
||||
className: 'conversation-search',
|
||||
initialize(options) {
|
||||
this.$input = options.input;
|
||||
this.$new_contact = this.$('.new-contact');
|
||||
|
||||
this.typeahead = new Whisper.ConversationCollection();
|
||||
this.collection = new Whisper.ConversationCollection([], {
|
||||
comparator(m) {
|
||||
return m.getTitle().toLowerCase();
|
||||
},
|
||||
});
|
||||
this.listenTo(this.collection, 'select', conversation => {
|
||||
this.resetTypeahead();
|
||||
this.trigger('open', conversation);
|
||||
});
|
||||
|
||||
// View to display the matched contacts from typeahead
|
||||
this.typeahead_view = new Whisper.ConversationListView({
|
||||
collection: this.collection,
|
||||
});
|
||||
this.$el.append(this.typeahead_view.el);
|
||||
this.initNewContact();
|
||||
this.pending = Promise.resolve();
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .new-contact': 'createConversation',
|
||||
},
|
||||
|
||||
filterContacts() {
|
||||
const query = this.$input.val().trim();
|
||||
if (query.length) {
|
||||
if (this.maybeNumber(query)) {
|
||||
this.new_contact_view.model.set('id', query);
|
||||
this.new_contact_view.render().$el.show();
|
||||
this.new_contact_view.validate();
|
||||
this.hideHints();
|
||||
} else {
|
||||
this.new_contact_view.$el.hide();
|
||||
}
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||
// to `async` / `await`:
|
||||
/* eslint-disable more/no-then */
|
||||
this.pending = this.pending.then(() =>
|
||||
this.typeahead.search(query).then(() => {
|
||||
let results = this.typeahead.filter(isSearchable);
|
||||
const noteToSelf = i18n('noteToSelf');
|
||||
if (noteToSelf.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const conversation = ConversationController.get(ourNumber);
|
||||
if (conversation) {
|
||||
// ensure that we don't have duplicates in our results
|
||||
results = results.filter(item => item.id !== ourNumber);
|
||||
results.unshift(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
this.typeahead_view.collection.reset(results);
|
||||
})
|
||||
);
|
||||
/* eslint-enable more/no-then */
|
||||
this.trigger('show');
|
||||
} else {
|
||||
this.resetTypeahead();
|
||||
}
|
||||
},
|
||||
|
||||
initNewContact() {
|
||||
if (this.new_contact_view) {
|
||||
this.new_contact_view.undelegateEvents();
|
||||
this.new_contact_view.$el.hide();
|
||||
}
|
||||
const model = new Whisper.Conversation({ type: 'private' });
|
||||
this.new_contact_view = new Whisper.NewContactView({
|
||||
el: this.$new_contact,
|
||||
model,
|
||||
}).render();
|
||||
},
|
||||
|
||||
async createConversation() {
|
||||
const isValidNumber = this.new_contact_view.model.isValid();
|
||||
if (!isValidNumber) {
|
||||
this.new_contact_view.$('.number').text(i18n('invalidNumberError'));
|
||||
this.$input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const newConversationId = this.new_contact_view.model.id;
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
newConversationId,
|
||||
'private'
|
||||
);
|
||||
this.trigger('open', conversation);
|
||||
this.initNewContact();
|
||||
this.resetTypeahead();
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.delegateEvents();
|
||||
this.typeahead_view.delegateEvents();
|
||||
this.new_contact_view.delegateEvents();
|
||||
this.resetTypeahead();
|
||||
},
|
||||
|
||||
resetTypeahead() {
|
||||
this.hideHints();
|
||||
this.new_contact_view.$el.hide();
|
||||
this.$input.val('').focus();
|
||||
this.typeahead_view.collection.reset([]);
|
||||
this.trigger('hide');
|
||||
},
|
||||
|
||||
showHints() {
|
||||
if (!this.hintView) {
|
||||
this.hintView = new Whisper.HintView({
|
||||
className: 'contact placeholder',
|
||||
content: i18n('newPhoneNumber'),
|
||||
}).render();
|
||||
this.hintView.$el.insertAfter(this.$input);
|
||||
}
|
||||
this.hintView.$el.show();
|
||||
},
|
||||
|
||||
hideHints() {
|
||||
if (this.hintView) {
|
||||
this.hintView.remove();
|
||||
this.hintView = null;
|
||||
}
|
||||
},
|
||||
|
||||
maybeNumber(number) {
|
||||
return number.replace(/[\s-.()]*/g, '').match(/^\+?[0-9]*$/);
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1377,11 +1377,7 @@
|
|||
},
|
||||
|
||||
async openConversation(number) {
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
number,
|
||||
'private'
|
||||
);
|
||||
window.Whisper.events.trigger('showConversation', conversation);
|
||||
window.Whisper.events.trigger('showConversation', number);
|
||||
},
|
||||
|
||||
listenBack(view) {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.HintView = Whisper.View.extend({
|
||||
templateName: 'hint',
|
||||
initialize(options) {
|
||||
this.content = options.content;
|
||||
},
|
||||
render_attributes() {
|
||||
return { content: this.content };
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,10 +1,12 @@
|
|||
/* global ConversationController: false */
|
||||
/* global extension: false */
|
||||
/* global getInboxCollection: false */
|
||||
/* global i18n: false */
|
||||
/* global Whisper: false */
|
||||
/* global textsecure: false */
|
||||
/* global Signal: false */
|
||||
/* global
|
||||
ConversationController,
|
||||
extension,
|
||||
getInboxCollection,
|
||||
i18n,
|
||||
Whisper,
|
||||
textsecure,
|
||||
Signal
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -38,38 +40,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
Whisper.FontSizeView = Whisper.View.extend({
|
||||
defaultSize: 14,
|
||||
maxSize: 30,
|
||||
minSize: 14,
|
||||
initialize() {
|
||||
this.currentSize = this.defaultSize;
|
||||
this.render();
|
||||
},
|
||||
events: { keydown: 'zoomText' },
|
||||
zoomText(e) {
|
||||
if (!e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
const keyCode = e.which || e.keyCode;
|
||||
const maxSize = 22; // if bigger text goes outside send-message textarea
|
||||
const minSize = 14;
|
||||
if (keyCode === 189 || keyCode === 109) {
|
||||
if (this.currentSize > minSize) {
|
||||
this.currentSize -= 1;
|
||||
}
|
||||
} else if (keyCode === 187 || keyCode === 107) {
|
||||
if (this.currentSize < maxSize) {
|
||||
this.currentSize += 1;
|
||||
}
|
||||
}
|
||||
this.render();
|
||||
},
|
||||
render() {
|
||||
this.$el.css('font-size', `${this.currentSize}px`);
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.AppLoadingScreen = Whisper.View.extend({
|
||||
templateName: 'app-loading-screen',
|
||||
className: 'app-loading-screen',
|
||||
|
@ -92,20 +62,6 @@
|
|||
this.render();
|
||||
this.$el.attr('tabindex', '1');
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Whisper.FontSizeView({ el: this.$el });
|
||||
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const me = ConversationController.getOrCreate(ourNumber, 'private');
|
||||
this.mainHeaderView = new Whisper.ReactWrapperView({
|
||||
className: 'main-header-wrapper',
|
||||
Component: Signal.Components.MainHeader,
|
||||
props: me.format(),
|
||||
});
|
||||
const update = () => this.mainHeaderView.update(me.format());
|
||||
this.listenTo(me, 'change', update);
|
||||
this.$('.main-header-placeholder').append(this.mainHeaderView.el);
|
||||
|
||||
this.conversation_stack = new Whisper.ConversationStack({
|
||||
el: this.$('.conversation-stack'),
|
||||
model: { window: options.window },
|
||||
|
@ -125,40 +81,6 @@
|
|||
this.networkStatusView.render();
|
||||
}
|
||||
});
|
||||
this.listenTo(inboxCollection, 'select', this.openConversation);
|
||||
|
||||
this.inboxListView = new Whisper.ConversationListView({
|
||||
el: this.$('.inbox'),
|
||||
collection: inboxCollection,
|
||||
}).render();
|
||||
|
||||
this.inboxListView.listenTo(
|
||||
inboxCollection,
|
||||
'add change:timestamp change:name change:number',
|
||||
this.inboxListView.updateLocation
|
||||
);
|
||||
this.inboxListView.listenTo(
|
||||
inboxCollection,
|
||||
'remove',
|
||||
this.inboxListView.removeItem
|
||||
);
|
||||
|
||||
this.searchView = new Whisper.ConversationSearchView({
|
||||
el: this.$('.search-results'),
|
||||
input: this.$('input.search'),
|
||||
});
|
||||
|
||||
this.searchView.$el.hide();
|
||||
|
||||
this.listenTo(this.searchView, 'hide', function toggleVisibility() {
|
||||
this.searchView.$el.hide();
|
||||
this.inboxListView.$el.show();
|
||||
});
|
||||
this.listenTo(this.searchView, 'show', function toggleVisibility() {
|
||||
this.searchView.$el.show();
|
||||
this.inboxListView.$el.hide();
|
||||
});
|
||||
this.listenTo(this.searchView, 'open', this.openConversation);
|
||||
|
||||
this.networkStatusView = new Whisper.NetworkStatusView();
|
||||
this.$el
|
||||
|
@ -170,18 +92,78 @@
|
|||
banner.$el.prependTo(this.$el);
|
||||
this.$el.addClass('expired');
|
||||
}
|
||||
|
||||
this.setupLeftPane();
|
||||
},
|
||||
render_attributes: {
|
||||
welcomeToSignal: i18n('welcomeToSignal'),
|
||||
selectAContact: i18n('selectAContact'),
|
||||
searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'),
|
||||
settings: i18n('settings'),
|
||||
},
|
||||
events: {
|
||||
click: 'onClick',
|
||||
'click #header': 'focusHeader',
|
||||
'click .conversation': 'focusConversation',
|
||||
'input input.search': 'filterContacts',
|
||||
},
|
||||
setupLeftPane() {
|
||||
// Here we set up a full redux store with initial state for our LeftPane Root
|
||||
const inboxCollection = getInboxCollection();
|
||||
const conversations = inboxCollection.map(
|
||||
conversation => conversation.cachedProps
|
||||
);
|
||||
const initialState = {
|
||||
conversations: {
|
||||
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
||||
},
|
||||
user: {
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourNumber: textsecure.storage.user.getNumber(),
|
||||
i18n: window.i18n,
|
||||
},
|
||||
};
|
||||
|
||||
this.store = Signal.State.createStore(initialState);
|
||||
window.inboxStore = this.store;
|
||||
this.leftPaneView = new Whisper.ReactWrapperView({
|
||||
JSX: Signal.State.Roots.createLeftPane(this.store),
|
||||
className: 'left-pane-wrapper',
|
||||
});
|
||||
|
||||
// Enables our redux store to be updated by backbone events in the outside world
|
||||
const {
|
||||
conversationAdded,
|
||||
conversationChanged,
|
||||
conversationRemoved,
|
||||
removeAllConversations,
|
||||
messageExpired,
|
||||
openConversationExternal,
|
||||
} = Signal.State.bindActionCreators(
|
||||
Signal.State.Ducks.conversations.actions,
|
||||
this.store.dispatch
|
||||
);
|
||||
const { userChanged } = Signal.State.bindActionCreators(
|
||||
Signal.State.Ducks.user.actions,
|
||||
this.store.dispatch
|
||||
);
|
||||
|
||||
this.openConversationAction = openConversationExternal;
|
||||
|
||||
this.listenTo(inboxCollection, 'remove', conversation => {
|
||||
const { id } = conversation || {};
|
||||
conversationRemoved(id);
|
||||
});
|
||||
this.listenTo(inboxCollection, 'add', conversation => {
|
||||
const { id, cachedProps } = conversation || {};
|
||||
conversationAdded(id, cachedProps);
|
||||
});
|
||||
this.listenTo(inboxCollection, 'change', conversation => {
|
||||
const { id, cachedProps } = conversation || {};
|
||||
conversationChanged(id, cachedProps);
|
||||
});
|
||||
this.listenTo(inboxCollection, 'reset', removeAllConversations);
|
||||
|
||||
Whisper.events.on('messageExpired', messageExpired);
|
||||
Whisper.events.on('userChanged', userChanged);
|
||||
|
||||
// Finally, add it to the DOM
|
||||
this.$('.left-pane-placeholder').append(this.leftPaneView.el);
|
||||
},
|
||||
startConnectionListener() {
|
||||
this.interval = setInterval(() => {
|
||||
|
@ -237,30 +219,18 @@
|
|||
reloadBackgroundPage() {
|
||||
window.location.reload();
|
||||
},
|
||||
filterContacts(e) {
|
||||
this.searchView.filterContacts(e);
|
||||
const input = this.$('input.search');
|
||||
if (input.val().length > 0) {
|
||||
input.addClass('active');
|
||||
const textDir = window.getComputedStyle(input[0]).direction;
|
||||
if (textDir === 'ltr') {
|
||||
input.removeClass('rtl').addClass('ltr');
|
||||
} else if (textDir === 'rtl') {
|
||||
input.removeClass('ltr').addClass('rtl');
|
||||
}
|
||||
} else {
|
||||
input.removeClass('active');
|
||||
}
|
||||
},
|
||||
openConversation(conversation) {
|
||||
this.searchView.hideHints();
|
||||
if (conversation) {
|
||||
ConversationController.markAsSelected(conversation);
|
||||
this.conversation_stack.open(
|
||||
ConversationController.get(conversation.id)
|
||||
);
|
||||
this.focusConversation();
|
||||
async openConversation(id, messageId) {
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
id,
|
||||
'private'
|
||||
);
|
||||
|
||||
if (this.openConversationAction) {
|
||||
this.openConversationAction(id, messageId);
|
||||
}
|
||||
|
||||
this.conversation_stack.open(conversation);
|
||||
this.focusConversation();
|
||||
},
|
||||
closeRecording(e) {
|
||||
if (e && this.$(e.target).closest('.capture-audio').length > 0) {
|
||||
|
|
|
@ -44,36 +44,36 @@
|
|||
getRenderInfo() {
|
||||
const { Components } = window.Signal;
|
||||
|
||||
if (this.model.isExpirationTimerUpdate()) {
|
||||
if (this.model.propsForTimerNotification) {
|
||||
return {
|
||||
Component: Components.TimerNotification,
|
||||
props: this.model.getPropsForTimerNotification(),
|
||||
props: this.model.propsForTimerNotification,
|
||||
};
|
||||
} else if (this.model.isKeyChange()) {
|
||||
} else if (this.model.propsForSafetyNumberNotification) {
|
||||
return {
|
||||
Component: Components.SafetyNumberNotification,
|
||||
props: this.model.getPropsForSafetyNumberNotification(),
|
||||
props: this.model.propsForSafetyNumberNotification,
|
||||
};
|
||||
} else if (this.model.isVerifiedChange()) {
|
||||
} else if (this.model.propsForVerificationNotification) {
|
||||
return {
|
||||
Component: Components.VerificationNotification,
|
||||
props: this.model.getPropsForVerificationNotification(),
|
||||
props: this.model.propsForVerificationNotification,
|
||||
};
|
||||
} else if (this.model.isEndSession()) {
|
||||
} else if (this.model.propsForResetSessionNotification) {
|
||||
return {
|
||||
Component: Components.ResetSessionNotification,
|
||||
props: this.model.getPropsForResetSessionNotification(),
|
||||
props: this.model.propsForResetSessionNotification,
|
||||
};
|
||||
} else if (this.model.isGroupUpdate()) {
|
||||
} else if (this.model.propsForGroupNotification) {
|
||||
return {
|
||||
Component: Components.GroupNotification,
|
||||
props: this.model.getPropsForGroupNotification(),
|
||||
props: this.model.propsForGroupNotification,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Component: Components.Message,
|
||||
props: this.model.getPropsForMessage(),
|
||||
props: this.model.propsForMessage,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
initialize(options) {
|
||||
const {
|
||||
Component,
|
||||
JSX,
|
||||
props,
|
||||
onClose,
|
||||
tagName,
|
||||
|
@ -28,6 +29,7 @@
|
|||
|
||||
this.tagName = tagName;
|
||||
this.className = className;
|
||||
this.JSX = JSX;
|
||||
this.Component = Component;
|
||||
this.onClose = onClose;
|
||||
this.onInitialRender = onInitialRender;
|
||||
|
@ -38,7 +40,9 @@
|
|||
},
|
||||
update(props) {
|
||||
const updatedProps = this.augmentProps(props);
|
||||
const reactElement = React.createElement(this.Component, updatedProps);
|
||||
const reactElement = this.JSX
|
||||
? this.JSX
|
||||
: React.createElement(this.Component, updatedProps);
|
||||
ReactDOM.render(reactElement, this.el, () => {
|
||||
if (this.hasRendered) {
|
||||
return;
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
/* global moment: false */
|
||||
/* global Whisper: false */
|
||||
/* global extension: false */
|
||||
/* global i18n: false */
|
||||
/* global _: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
function extendedRelativeTime(number, string) {
|
||||
return moment.duration(-1 * number, string).humanize(string !== 's');
|
||||
}
|
||||
|
||||
const extendedFormats = {
|
||||
y: 'lll',
|
||||
M: `${i18n('timestampFormat_M') || 'MMM D'} LT`,
|
||||
d: 'ddd LT',
|
||||
};
|
||||
|
||||
function shortRelativeTime(number, string) {
|
||||
return moment.duration(number, string).humanize();
|
||||
}
|
||||
const shortFormats = {
|
||||
y: 'll',
|
||||
M: i18n('timestampFormat_M') || 'MMM D',
|
||||
d: 'ddd',
|
||||
};
|
||||
|
||||
function getRelativeTimeSpanString(rawTimestamp, options = {}) {
|
||||
_.defaults(options, { extended: false });
|
||||
|
||||
const relativeTime = options.extended
|
||||
? extendedRelativeTime
|
||||
: shortRelativeTime;
|
||||
const formats = options.extended ? extendedFormats : shortFormats;
|
||||
|
||||
// Convert to moment timestamp if it isn't already
|
||||
const timestamp = moment(rawTimestamp);
|
||||
const now = moment();
|
||||
const timediff = moment.duration(now - timestamp);
|
||||
|
||||
if (timediff.years() > 0) {
|
||||
return timestamp.format(formats.y);
|
||||
} else if (timediff.months() > 0 || timediff.days() > 6) {
|
||||
return timestamp.format(formats.M);
|
||||
} else if (timediff.days() > 0) {
|
||||
return timestamp.format(formats.d);
|
||||
} else if (timediff.hours() >= 1) {
|
||||
return relativeTime(timediff.hours(), 'h');
|
||||
} else if (timediff.minutes() >= 1) {
|
||||
// Note that humanize seems to jump to '1 hour' as soon as we cross 45 minutes
|
||||
return relativeTime(timediff.minutes(), 'm');
|
||||
}
|
||||
|
||||
return relativeTime(timediff.seconds(), 's');
|
||||
}
|
||||
|
||||
Whisper.TimestampView = Whisper.View.extend({
|
||||
initialize() {
|
||||
extension.windows.onClosed(this.clearTimeout.bind(this));
|
||||
},
|
||||
update() {
|
||||
this.clearTimeout();
|
||||
const millisNow = Date.now();
|
||||
let millis = this.$el.data('timestamp');
|
||||
if (millis === '') {
|
||||
return;
|
||||
}
|
||||
if (millis >= millisNow) {
|
||||
millis = millisNow;
|
||||
}
|
||||
const result = this.getRelativeTimeSpanString(millis);
|
||||
this.delay = this.getDelay(millis);
|
||||
this.$el.text(result);
|
||||
|
||||
const timestamp = moment(millis);
|
||||
this.$el.attr('title', timestamp.format('llll'));
|
||||
|
||||
if (this.delay) {
|
||||
if (this.delay < 0) {
|
||||
this.delay = 1000;
|
||||
}
|
||||
this.timeout = setTimeout(this.update.bind(this), this.delay);
|
||||
}
|
||||
},
|
||||
clearTimeout() {
|
||||
clearTimeout(this.timeout);
|
||||
},
|
||||
getRelativeTimeSpanString(timestamp) {
|
||||
return getRelativeTimeSpanString(timestamp);
|
||||
},
|
||||
getDelay(rawTimestamp) {
|
||||
// Convert to moment timestamp if it isn't already
|
||||
const timestamp = moment(rawTimestamp);
|
||||
const now = moment();
|
||||
const timediff = moment.duration(now - timestamp);
|
||||
|
||||
if (timediff.years() > 0) {
|
||||
return null;
|
||||
} else if (timediff.months() > 0 || timediff.days() > 6) {
|
||||
return null;
|
||||
} else if (timediff.days() > 0) {
|
||||
return moment(timestamp)
|
||||
.add(timediff.days() + 1, 'd')
|
||||
.diff(now);
|
||||
} else if (timediff.hours() >= 1) {
|
||||
return moment(timestamp)
|
||||
.add(timediff.hours() + 1, 'h')
|
||||
.diff(now);
|
||||
} else if (timediff.minutes() >= 1) {
|
||||
return moment(timestamp)
|
||||
.add(timediff.minutes() + 1, 'm')
|
||||
.diff(now);
|
||||
}
|
||||
|
||||
return moment(timestamp)
|
||||
.add(1, 'm')
|
||||
.diff(now);
|
||||
},
|
||||
});
|
||||
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({
|
||||
getRelativeTimeSpanString(timestamp) {
|
||||
return getRelativeTimeSpanString(timestamp, { extended: true });
|
||||
},
|
||||
});
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue