Show notifications when a user's profile name changes
This commit is contained in:
parent
2f015863ca
commit
d75eee015f
44 changed files with 749 additions and 194 deletions
|
@ -346,12 +346,16 @@
|
||||||
"description": "When there are multiple previously-verified group members with safety number changes, a banner will be shown. The list of contacts with safety number changes is shown, and this text introduces that list."
|
"description": "When there are multiple previously-verified group members with safety number changes, a banner will be shown. The list of contacts with safety number changes is shown, and this text introduces that list."
|
||||||
},
|
},
|
||||||
"changedRightAfterVerify": {
|
"changedRightAfterVerify": {
|
||||||
"message": "The safety number you are trying to verify has changed. Please review your new safety number with $name$. Remember, this change could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
|
"message": "The safety number you are trying to verify has changed. Please review your new safety number with $name1$. Remember, this change could mean that someone is trying to intercept your communication or that $name2$ has simply reinstalled Signal.",
|
||||||
"description": "Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change",
|
"description": "Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name1": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "Bob"
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"name2": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Bob"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -360,12 +364,16 @@
|
||||||
"description": "Shown on confirmation dialog when user attempts to send a message"
|
"description": "Shown on confirmation dialog when user attempts to send a message"
|
||||||
},
|
},
|
||||||
"identityKeyErrorOnSend": {
|
"identityKeyErrorOnSend": {
|
||||||
"message": "Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.",
|
"message": "Your safety number with $name1$ has changed. This could either mean that someone is trying to intercept your communication or that $name2$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.",
|
||||||
"description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
|
"description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name1": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "Bob"
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"name2": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Bob"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -549,7 +557,7 @@
|
||||||
"message": "When including a non-image attachment, the limit is one attachment per message.",
|
"message": "When including a non-image attachment, the limit is one attachment per message.",
|
||||||
"description": "An error popup when the user has attempted to add an attachment"
|
"description": "An error popup when the user has attempted to add an attachment"
|
||||||
},
|
},
|
||||||
"cannotMixImageAdnNonImageAttachments": {
|
"cannotMixImageAndNonImageAttachments": {
|
||||||
"message": "You cannot mix non-image and image attachments in one message.",
|
"message": "You cannot mix non-image and image attachments in one message.",
|
||||||
"description": "An error popup when the user has attempted to add an attachment"
|
"description": "An error popup when the user has attempted to add an attachment"
|
||||||
},
|
},
|
||||||
|
@ -687,7 +695,7 @@
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "dog"
|
"example": "dog"
|
||||||
},
|
},
|
||||||
"searchTerm": {
|
"conversationName": {
|
||||||
"content": "$2",
|
"content": "$2",
|
||||||
"example": "Friends"
|
"example": "Friends"
|
||||||
}
|
}
|
||||||
|
@ -1378,7 +1386,7 @@
|
||||||
"description": "Brief message shown when trying to message a blocked group"
|
"description": "Brief message shown when trying to message a blocked group"
|
||||||
},
|
},
|
||||||
"youChangedTheTimer": {
|
"youChangedTheTimer": {
|
||||||
"message": "You set the disappearing message timer to $time$",
|
"message": "You set the disappearing message time to $time$.",
|
||||||
"description": "Message displayed when you change the message expiration timer in a conversation.",
|
"description": "Message displayed when you change the message expiration timer in a conversation.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"time": {
|
"time": {
|
||||||
|
@ -1388,7 +1396,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timerSetOnSync": {
|
"timerSetOnSync": {
|
||||||
"message": "Updated disappearing message timer to $time$",
|
"message": "Updated the disappearing message time to $time$.",
|
||||||
"description": "Message displayed when timer is set on initial link of desktop device.",
|
"description": "Message displayed when timer is set on initial link of desktop device.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"time": {
|
"time": {
|
||||||
|
@ -1398,7 +1406,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"theyChangedTheTimer": {
|
"theyChangedTheTimer": {
|
||||||
"message": "$name$ set the disappearing message timer to $time$",
|
"message": "$name$ set the disappearing message time to $time$.",
|
||||||
"description": "Message displayed when someone else changes the message expiration timer in a conversation.",
|
"description": "Message displayed when someone else changes the message expiration timer in a conversation.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
|
@ -1516,7 +1524,7 @@
|
||||||
"description": "Displayed in the left pane when the timer is turned off"
|
"description": "Displayed in the left pane when the timer is turned off"
|
||||||
},
|
},
|
||||||
"disabledDisappearingMessages": {
|
"disabledDisappearingMessages": {
|
||||||
"message": "$name$ disabled disappearing messages",
|
"message": "$name$ disabled disappearing messages.",
|
||||||
"description": "Displayed in the conversation list when the timer is turned off",
|
"description": "Displayed in the conversation list when the timer is turned off",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
|
@ -1526,7 +1534,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"youDisabledDisappearingMessages": {
|
"youDisabledDisappearingMessages": {
|
||||||
"message": "You disabled disappearing messages",
|
"message": "You disabled disappearing messages.",
|
||||||
"description": "Displayed in the conversation list when the timer is turned off"
|
"description": "Displayed in the conversation list when the timer is turned off"
|
||||||
},
|
},
|
||||||
"timerSetTo": {
|
"timerSetTo": {
|
||||||
|
@ -1555,6 +1563,38 @@
|
||||||
"message": "Enable incoming calls",
|
"message": "Enable incoming calls",
|
||||||
"description": "Description for incoming calls setting"
|
"description": "Description for incoming calls setting"
|
||||||
},
|
},
|
||||||
|
"contactChangedProfileName": {
|
||||||
|
"message": "$sender$ changed their profile name from $oldProfile$ to $newProfile$.",
|
||||||
|
"description": "Description for incoming calls setting",
|
||||||
|
"placeholders": {
|
||||||
|
"sender": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"oldProfile": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": ".x8Skillz8x."
|
||||||
|
},
|
||||||
|
"newProfile": {
|
||||||
|
"content": "$3",
|
||||||
|
"example": "Bob Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"changedProfileName": {
|
||||||
|
"message": "$oldProfile$ changed their profile name to $newProfile$.",
|
||||||
|
"description": "Shown when a contact not in your address book changes their profile name",
|
||||||
|
"placeholders": {
|
||||||
|
"oldProfile": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": ".x8Skillz8x."
|
||||||
|
},
|
||||||
|
"newProfile": {
|
||||||
|
"content": "$3",
|
||||||
|
"example": "Bob Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"safetyNumberChanged": {
|
"safetyNumberChanged": {
|
||||||
"message": "Safety Number has changed",
|
"message": "Safety Number has changed",
|
||||||
"description": "A notification shown in the conversation when a contact reinstalls"
|
"description": "A notification shown in the conversation when a contact reinstalls"
|
||||||
|
@ -1578,10 +1618,10 @@
|
||||||
"description": "Label on button included with safety number change notification in the conversation"
|
"description": "Label on button included with safety number change notification in the conversation"
|
||||||
},
|
},
|
||||||
"yourSafetyNumberWith": {
|
"yourSafetyNumberWith": {
|
||||||
"message": "Your safety number with $name$:",
|
"message": "Your safety number with $name1$:",
|
||||||
"description": "Heading for safety number view",
|
"description": "Heading for safety number view",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name1": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "John"
|
"example": "John"
|
||||||
}
|
}
|
||||||
|
@ -2353,27 +2393,27 @@
|
||||||
"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": {
|
"MessageRequests--message-direct": {
|
||||||
"message": "Do you want to let $name$ message you? They won't know you've seen their message until you accept.",
|
"message": "Let $name$ message you and share your name and photo with them? They won’t know you’ve seen their messages until you accept.",
|
||||||
"description": "Shown as the message for a message request in a direct message",
|
"description": "Shown as the message for a message request in a direct message",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "Cayce Pollard"
|
"example": "Cayce"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MessageRequests--message-direct-blocked": {
|
"MessageRequests--message-direct-blocked": {
|
||||||
"message": "Unblock $name$ to message and call each other.",
|
"message": "Let $name$ message you and share your name and photo with them? You won't receive any messages until you unblock them.",
|
||||||
"description": "Shown as the message for a message request in a direct message with a blocked account",
|
"description": "Shown as the message for a message request in a direct message with a blocked account",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "Cayce Pollard"
|
"example": "Cayce"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MessageRequests--message-group": {
|
"MessageRequests--message-group": {
|
||||||
"message": "Do you want to join $group$? They won't know you've seen their message until you accept.",
|
"message": "Join this group and share your name and photo with its members? They won’t know you’ve seen their messages until you accept.",
|
||||||
"description": "Shown as the message for a message request in a group",
|
"description": "Shown as the message for a message request in a group",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
|
@ -2383,7 +2423,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MessageRequests--message-group-blocked": {
|
"MessageRequests--message-group-blocked": {
|
||||||
"message": "Unblock to allow group members to add you to this group again.",
|
"message": "Unblock this group and share your name and photo with its members? You won't receive any messages until you unblock them.",
|
||||||
"description": "Shown as the message for a message request in a blocked group"
|
"description": "Shown as the message for a message request in a blocked group"
|
||||||
},
|
},
|
||||||
"MessageRequests--block": {
|
"MessageRequests--block": {
|
||||||
|
|
|
@ -479,6 +479,7 @@
|
||||||
typingContact: typingContact ? typingContact.format() : null,
|
typingContact: typingContact ? typingContact.format() : null,
|
||||||
lastUpdated: this.get('timestamp'),
|
lastUpdated: this.get('timestamp'),
|
||||||
name: this.get('name'),
|
name: this.get('name'),
|
||||||
|
firstName: this.get('profileName'),
|
||||||
profileName: this.getProfileName(),
|
profileName: this.getProfileName(),
|
||||||
timestamp,
|
timestamp,
|
||||||
inboxPosition,
|
inboxPosition,
|
||||||
|
@ -1081,9 +1082,43 @@
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.trigger('newmessage', model);
|
this.trigger('newmessage', model);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async addProfileChange(profileChange, conversationId) {
|
||||||
|
const message = {
|
||||||
|
conversationId: this.id,
|
||||||
|
type: 'profile-change',
|
||||||
|
sent_at: Date.now(),
|
||||||
|
received_at: Date.now(),
|
||||||
|
unread: true,
|
||||||
|
changedId: conversationId || this.id,
|
||||||
|
profileChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = await window.Signal.Data.saveMessage(message, {
|
||||||
|
Message: Whisper.Message,
|
||||||
|
});
|
||||||
|
const model = MessageController.register(
|
||||||
|
id,
|
||||||
|
new Whisper.Message({
|
||||||
|
...message,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.trigger('newmessage', model);
|
||||||
|
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
ConversationController.getAllGroupsInvolvingId(this.id).then(groups => {
|
||||||
|
_.forEach(groups, group => {
|
||||||
|
group.addProfileChange(profileChange, this.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async onReadMessage(message, readAt) {
|
async onReadMessage(message, readAt) {
|
||||||
// We mark as read everything older than this message - to clean up old stuff
|
// We mark as read everything older than this message - to clean up old stuff
|
||||||
// still marked unread in the database. If the user generally doesn't read in
|
// still marked unread in the database. If the user generally doesn't read in
|
||||||
|
@ -2489,8 +2524,28 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
// encode
|
// encode
|
||||||
const profileFamilyName = family ? stringFromBytes(family) : null;
|
|
||||||
const profileName = given ? stringFromBytes(given) : null;
|
const profileName = given ? stringFromBytes(given) : null;
|
||||||
|
const profileFamilyName = family ? stringFromBytes(family) : null;
|
||||||
|
|
||||||
|
// check for changes
|
||||||
|
const oldName = this.getProfileName();
|
||||||
|
const newName = Util.combineNames(profileName, profileFamilyName);
|
||||||
|
const hadPreviousName = Boolean(oldName);
|
||||||
|
|
||||||
|
// Note that we compare the combined names to ensure that we don't present the exact
|
||||||
|
// same before/after string, even if someone is moving from just first name to
|
||||||
|
// first/last name in their profile data.
|
||||||
|
const nameChanged = oldName !== newName;
|
||||||
|
|
||||||
|
if (!this.isMe() && hadPreviousName && nameChanged) {
|
||||||
|
const change = {
|
||||||
|
type: 'name',
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addProfileChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
// set
|
// set
|
||||||
this.set({ profileName, profileFamilyName });
|
this.set({ profileName, profileFamilyName });
|
||||||
|
|
|
@ -183,6 +183,11 @@
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: this.getPropsForCallHistory(),
|
data: this.getPropsForCallHistory(),
|
||||||
};
|
};
|
||||||
|
} else if (this.isProfileChange()) {
|
||||||
|
return {
|
||||||
|
type: 'profileChange',
|
||||||
|
data: this.getPropsForProfileChange(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -364,6 +369,9 @@
|
||||||
isCallHistory() {
|
isCallHistory() {
|
||||||
return this.get('type') === 'call-history';
|
return this.get('type') === 'call-history';
|
||||||
},
|
},
|
||||||
|
isProfileChange() {
|
||||||
|
return this.get('type') === 'profile-change';
|
||||||
|
},
|
||||||
|
|
||||||
// Props for each message type
|
// Props for each message type
|
||||||
getPropsForUnsupportedMessage() {
|
getPropsForUnsupportedMessage() {
|
||||||
|
@ -508,6 +516,16 @@
|
||||||
callHistoryDetails: this.get('callHistoryDetails'),
|
callHistoryDetails: this.get('callHistoryDetails'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getPropsForProfileChange() {
|
||||||
|
const change = this.get('profileChange');
|
||||||
|
const changedId = this.get('changedId');
|
||||||
|
|
||||||
|
return {
|
||||||
|
changedContact: this.findAndFormatContact(changedId),
|
||||||
|
change,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getAttachmentsForMessage() {
|
getAttachmentsForMessage() {
|
||||||
const sticker = this.get('sticker');
|
const sticker = this.get('sticker');
|
||||||
if (sticker && sticker.data) {
|
if (sticker && sticker.data) {
|
||||||
|
@ -856,6 +874,17 @@
|
||||||
if (this.isUnsupportedMessage()) {
|
if (this.isUnsupportedMessage()) {
|
||||||
return i18n('message--getDescription--unsupported-message');
|
return i18n('message--getDescription--unsupported-message');
|
||||||
}
|
}
|
||||||
|
if (this.isProfileChange()) {
|
||||||
|
const change = this.get('profileChange');
|
||||||
|
const changedId = this.get('changedId');
|
||||||
|
const changedContact = this.findAndFormatContact(changedId);
|
||||||
|
|
||||||
|
return Signal.Util.getStringForProfileChange(
|
||||||
|
change,
|
||||||
|
changedContact,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
}
|
||||||
if (this.isTapToView()) {
|
if (this.isTapToView()) {
|
||||||
if (this.isErased()) {
|
if (this.isErased()) {
|
||||||
return i18n('message--getDescription--disappearing-media');
|
return i18n('message--getDescription--disappearing-media');
|
||||||
|
@ -884,7 +913,9 @@
|
||||||
if (groupUpdate.left === 'You') {
|
if (groupUpdate.left === 'You') {
|
||||||
return i18n('youLeftTheGroup');
|
return i18n('youLeftTheGroup');
|
||||||
} else if (groupUpdate.left) {
|
} else if (groupUpdate.left) {
|
||||||
return i18n('leftTheGroup', this.getNameForNumber(groupUpdate.left));
|
return i18n('leftTheGroup', [
|
||||||
|
this.getNameForNumber(groupUpdate.left),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fromContact) {
|
if (!fromContact) {
|
||||||
|
@ -894,7 +925,7 @@
|
||||||
if (fromContact.isMe()) {
|
if (fromContact.isMe()) {
|
||||||
messages.push(i18n('youUpdatedTheGroup'));
|
messages.push(i18n('youUpdatedTheGroup'));
|
||||||
} else {
|
} else {
|
||||||
messages.push(i18n('updatedTheGroup', fromContact.getTitle()));
|
messages.push(i18n('updatedTheGroup', [fromContact.getTitle()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupUpdate.joined && groupUpdate.joined.length) {
|
if (groupUpdate.joined && groupUpdate.joined.length) {
|
||||||
|
@ -907,10 +938,11 @@
|
||||||
|
|
||||||
if (joinedContacts.length > 1) {
|
if (joinedContacts.length > 1) {
|
||||||
messages.push(
|
messages.push(
|
||||||
i18n(
|
i18n('multipleJoinedTheGroup', [
|
||||||
'multipleJoinedTheGroup',
|
_.map(joinedWithoutMe, contact => contact.getTitle()).join(
|
||||||
_.map(joinedWithoutMe, contact => contact.getTitle()).join(', ')
|
', '
|
||||||
)
|
),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
if (joinedWithoutMe.length < joinedContacts.length) {
|
if (joinedWithoutMe.length < joinedContacts.length) {
|
||||||
|
@ -925,14 +957,14 @@
|
||||||
messages.push(i18n('youJoinedTheGroup'));
|
messages.push(i18n('youJoinedTheGroup'));
|
||||||
} else {
|
} else {
|
||||||
messages.push(
|
messages.push(
|
||||||
i18n('joinedTheGroup', joinedContacts[0].getTitle())
|
i18n('joinedTheGroup', [joinedContacts[0].getTitle()])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupUpdate.name) {
|
if (groupUpdate.name) {
|
||||||
messages.push(i18n('titleIsNow', groupUpdate.name));
|
messages.push(i18n('titleIsNow', [groupUpdate.name]));
|
||||||
}
|
}
|
||||||
if (groupUpdate.avatarUpdated) {
|
if (groupUpdate.avatarUpdated) {
|
||||||
messages.push(i18n('updatedGroupAvatar'));
|
messages.push(i18n('updatedGroupAvatar'));
|
||||||
|
@ -965,18 +997,16 @@
|
||||||
return i18n('disappearingMessagesDisabled');
|
return i18n('disappearingMessagesDisabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18n(
|
return i18n('timerSetTo', [
|
||||||
'timerSetTo',
|
Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0),
|
||||||
Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0)
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (this.isKeyChange()) {
|
if (this.isKeyChange()) {
|
||||||
const identifier = this.get('key_changed');
|
const identifier = this.get('key_changed');
|
||||||
const conversation = this.findContact(identifier);
|
const conversation = this.findContact(identifier);
|
||||||
return i18n(
|
return i18n('safetyNumberChangedGroup', [
|
||||||
'safetyNumberChangedGroup',
|
conversation ? conversation.getTitle() : null,
|
||||||
conversation ? conversation.getTitle() : null
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const contacts = this.get('contact');
|
const contacts = this.get('contact');
|
||||||
if (contacts && contacts.length) {
|
if (contacts && contacts.length) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
/* eslint-env node */
|
/* eslint-env node, browser */
|
||||||
/* global log */
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
const log = typeof window !== 'undefined' ? window.log : console;
|
||||||
|
|
||||||
exports.setup = (locale, messages) => {
|
exports.setup = (locale, messages) => {
|
||||||
if (!locale) {
|
if (!locale) {
|
||||||
|
@ -17,18 +19,57 @@ exports.setup = (locale, messages) => {
|
||||||
);
|
);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(substitutions) && substitutions.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
'Array syntax is not supported with more than one placeholder'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof substitutions === 'string' ||
|
||||||
|
typeof substitutions === 'number'
|
||||||
|
) {
|
||||||
|
throw new Error('You must provide either a map or an array');
|
||||||
|
}
|
||||||
|
|
||||||
const { message } = entry;
|
const { message } = entry;
|
||||||
if (Array.isArray(substitutions)) {
|
if (!substitutions) {
|
||||||
|
return message;
|
||||||
|
} else if (Array.isArray(substitutions)) {
|
||||||
return substitutions.reduce(
|
return substitutions.reduce(
|
||||||
(result, substitution) => result.replace(/\$.+?\$/, substitution),
|
(result, substitution) => result.replace(/\$.+?\$/, substitution),
|
||||||
message
|
message
|
||||||
);
|
);
|
||||||
} else if (substitutions) {
|
|
||||||
return message.replace(/\$.+?\$/, substitutions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
|
||||||
|
|
||||||
|
let match = FIND_REPLACEMENTS.exec(message);
|
||||||
|
let builder = '';
|
||||||
|
let lastTextIndex = 0;
|
||||||
|
|
||||||
|
while (match) {
|
||||||
|
if (lastTextIndex < match.index) {
|
||||||
|
builder += message.slice(lastTextIndex, match.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderName = match[1];
|
||||||
|
const value = substitutions[placeholderName];
|
||||||
|
if (!value) {
|
||||||
|
log.error(
|
||||||
|
`i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
builder += value || '';
|
||||||
|
|
||||||
|
lastTextIndex = FIND_REPLACEMENTS.lastIndex;
|
||||||
|
match = FIND_REPLACEMENTS.exec(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTextIndex < message.length) {
|
||||||
|
builder += message.slice(lastTextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMessage.getLocale = () => locale;
|
getMessage.getLocale = () => locale;
|
||||||
|
|
|
@ -93,18 +93,18 @@
|
||||||
iconUrl = last.iconUrl;
|
iconUrl = last.iconUrl;
|
||||||
if (numNotifications === 1) {
|
if (numNotifications === 1) {
|
||||||
if (last.reaction) {
|
if (last.reaction) {
|
||||||
message = i18n('notificationReaction', [
|
message = i18n('notificationReaction', {
|
||||||
lastMessageTitle,
|
sender: lastMessageTitle,
|
||||||
last.reaction.emoji,
|
emoji: last.reaction.emoji,
|
||||||
]);
|
});
|
||||||
} else {
|
} else {
|
||||||
message = `${i18n('notificationFrom')} ${lastMessageTitle}`;
|
message = `${i18n('notificationFrom')} ${lastMessageTitle}`;
|
||||||
}
|
}
|
||||||
} else if (last.reaction) {
|
} else if (last.reaction) {
|
||||||
message = i18n('notificationReactionMostRecent', [
|
message = i18n('notificationReactionMostRecent', {
|
||||||
lastMessageTitle,
|
sender: lastMessageTitle,
|
||||||
last.reaction.emoji,
|
emoji: last.reaction.emoji,
|
||||||
]);
|
});
|
||||||
} else {
|
} else {
|
||||||
message = `${i18n(
|
message = `${i18n(
|
||||||
'notificationMostRecentFrom'
|
'notificationMostRecentFrom'
|
||||||
|
@ -117,22 +117,22 @@
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
title = last.title;
|
title = last.title;
|
||||||
if (last.reaction) {
|
if (last.reaction) {
|
||||||
message = i18n('notificationReactionMessage', [
|
message = i18n('notificationReactionMessage', {
|
||||||
last.title,
|
sender: last.title,
|
||||||
last.reaction.emoji,
|
emoji: last.reaction.emoji,
|
||||||
last.message,
|
message: last.message,
|
||||||
]);
|
});
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
message = last.message;
|
message = last.message;
|
||||||
}
|
}
|
||||||
} else if (last.reaction) {
|
} else if (last.reaction) {
|
||||||
title = newMessageCountLabel;
|
title = newMessageCountLabel;
|
||||||
message = i18n('notificationReactionMessageMostRecent', [
|
message = i18n('notificationReactionMessageMostRecent', {
|
||||||
last.title,
|
sender: last.title,
|
||||||
last.reaction.emoji,
|
emoji: last.reaction.emoji,
|
||||||
last.message,
|
message: last.message,
|
||||||
]);
|
});
|
||||||
} else {
|
} else {
|
||||||
title = newMessageCountLabel;
|
title = newMessageCountLabel;
|
||||||
message = `${i18n('notificationMostRecent')} ${last.message}`;
|
message = `${i18n('notificationMostRecent')} ${last.message}`;
|
||||||
|
|
|
@ -210,7 +210,7 @@
|
||||||
template: i18n('oneNonImageAtATimeToast'),
|
template: i18n('oneNonImageAtATimeToast'),
|
||||||
});
|
});
|
||||||
Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
|
Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
|
||||||
template: i18n('cannotMixImageAdnNonImageAttachments'),
|
template: i18n('cannotMixImageAndNonImageAttachments'),
|
||||||
});
|
});
|
||||||
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
||||||
template: i18n('maximumAttachments'),
|
template: i18n('maximumAttachments'),
|
||||||
|
@ -1655,7 +1655,7 @@
|
||||||
if (unverified.length > 1) {
|
if (unverified.length > 1) {
|
||||||
message = i18n('multipleNoLongerVerified');
|
message = i18n('multipleNoLongerVerified');
|
||||||
} else {
|
} else {
|
||||||
message = i18n('noLongerVerified', unverified.at(0).getTitle());
|
message = i18n('noLongerVerified', [unverified.at(0).getTitle()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to re-add, since unverified set may have changed
|
// Need to re-add, since unverified set may have changed
|
||||||
|
@ -2030,10 +2030,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = new Whisper.ConfirmationDialogView({
|
const dialog = new Whisper.ConfirmationDialogView({
|
||||||
message: i18n('identityKeyErrorOnSend', [
|
message: i18n('identityKeyErrorOnSend', {
|
||||||
contact.getTitle(),
|
name1: contact.getTitle(),
|
||||||
contact.getTitle(),
|
name2: contact.getTitle(),
|
||||||
]),
|
}),
|
||||||
okText: i18n('sendAnyway'),
|
okText: i18n('sendAnyway'),
|
||||||
resolve: async () => {
|
resolve: async () => {
|
||||||
await contact.updateVerified();
|
await contact.updateVerified();
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
className: 'app-loading-screen',
|
className: 'app-loading-screen',
|
||||||
updateProgress(count) {
|
updateProgress(count) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const message = i18n('loadingMessages', count.toString());
|
const message = i18n('loadingMessages', [count.toString()]);
|
||||||
this.$('.message').text(message);
|
this.$('.message').text(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -57,7 +57,10 @@ export const UploadStage = () => {
|
||||||
<div className={styles.base}>
|
<div className={styles.base}>
|
||||||
<H2>{i18n('StickerCreator--UploadStage--title')}</H2>
|
<H2>{i18n('StickerCreator--UploadStage--title')}</H2>
|
||||||
<Text>
|
<Text>
|
||||||
{i18n('StickerCreator--UploadStage-uploaded', [complete, total])}
|
{i18n('StickerCreator--UploadStage-uploaded', {
|
||||||
|
count: complete,
|
||||||
|
total,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
count={complete}
|
count={complete}
|
||||||
|
|
|
@ -2,9 +2,13 @@ import * as React from 'react';
|
||||||
|
|
||||||
export type I18nFn = (
|
export type I18nFn = (
|
||||||
key: string,
|
key: string,
|
||||||
substitutions?: Array<string | number>
|
substitutions?: Array<string | number> | ReplacementValuesType
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
export type ReplacementValuesType = {
|
||||||
|
[key: string]: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
const I18nContext = React.createContext<I18nFn>(() => 'NO LOCALE LOADED');
|
const I18nContext = React.createContext<I18nFn>(() => 'NO LOCALE LOADED');
|
||||||
|
|
||||||
export type I18nProps = {
|
export type I18nProps = {
|
||||||
|
@ -14,11 +18,55 @@ export type I18nProps = {
|
||||||
|
|
||||||
export const I18n = ({ messages, children }: I18nProps) => {
|
export const I18n = ({ messages, children }: I18nProps) => {
|
||||||
const getMessage = React.useCallback<I18nFn>(
|
const getMessage = React.useCallback<I18nFn>(
|
||||||
(key, substitutions = []) =>
|
(key, substitutions) => {
|
||||||
substitutions.reduce<string>(
|
if (Array.isArray(substitutions) && substitutions.length > 1) {
|
||||||
(res, sub) => res.replace(/\$.+?\$/, sub),
|
throw new Error(
|
||||||
messages[key].message
|
'Array syntax is not supported with more than one placeholder'
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { message } = messages[key];
|
||||||
|
if (!substitutions) {
|
||||||
|
return message;
|
||||||
|
} else if (Array.isArray(substitutions)) {
|
||||||
|
return substitutions.reduce(
|
||||||
|
(result, substitution) =>
|
||||||
|
result.toString().replace(/\$.+?\$/, substitution.toString()),
|
||||||
|
message
|
||||||
|
) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
|
||||||
|
|
||||||
|
let match = FIND_REPLACEMENTS.exec(message);
|
||||||
|
let builder = '';
|
||||||
|
let lastTextIndex = 0;
|
||||||
|
|
||||||
|
while (match) {
|
||||||
|
if (lastTextIndex < match.index) {
|
||||||
|
builder += message.slice(lastTextIndex, match.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderName = match[1];
|
||||||
|
const value = substitutions[placeholderName];
|
||||||
|
if (!value) {
|
||||||
|
// tslint:disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
`i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
builder += value || '';
|
||||||
|
|
||||||
|
lastTextIndex = FIND_REPLACEMENTS.lastIndex;
|
||||||
|
match = FIND_REPLACEMENTS.exec(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTextIndex < message.length) {
|
||||||
|
builder += message.slice(lastTextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
},
|
||||||
[messages]
|
[messages]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -8795,6 +8795,45 @@ button.module-image__border-overlay:focus {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: Profile Change Notification
|
||||||
|
|
||||||
|
.module-profile-change-notification {
|
||||||
|
@include font-body-1;
|
||||||
|
|
||||||
|
margin-left: 2em;
|
||||||
|
margin-right: 2em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-profile-change-notification--icon {
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/profile-outline-20.svg',
|
||||||
|
$color-gray-60
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/profile-outline-20.svg',
|
||||||
|
$color-gray-05
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Third-party module: react-tooltip-lite */
|
/* Third-party module: react-tooltip-lite */
|
||||||
|
|
||||||
.react-tooltip-lite {
|
.react-tooltip-lite {
|
||||||
|
|
|
@ -6,20 +6,25 @@ describe('i18n', () => {
|
||||||
assert.strictEqual(i18n('random'), '');
|
assert.strictEqual(i18n('random'), '');
|
||||||
});
|
});
|
||||||
it('returns message for given string', () => {
|
it('returns message for given string', () => {
|
||||||
assert.equal(i18n('reportIssue'), 'Report an issue');
|
assert.equal(i18n('reportIssue'), ['Report an issue']);
|
||||||
});
|
});
|
||||||
it('returns message with single substitution', () => {
|
it('returns message with single substitution', () => {
|
||||||
const actual = i18n('cannotUpdateDetail', 'https://signal.org/download');
|
const actual = i18n('cannotUpdateDetail', [
|
||||||
|
'https://signal.org/download',
|
||||||
|
]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
actual,
|
actual,
|
||||||
'Signal Desktop failed to update, but there is a new version available. Please go to https://signal.org/download and install the new version manually, then either contact support or file a bug about this problem.'
|
'Signal Desktop failed to update, but there is a new version available. Please go to https://signal.org/download and install the new version manually, then either contact support or file a bug about this problem.'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('returns message with multiple substitutions', () => {
|
it('returns message with multiple substitutions', () => {
|
||||||
const actual = i18n('theyChangedTheTimer', ['Someone', '5 minutes']);
|
const actual = i18n('theyChangedTheTimer', {
|
||||||
|
name: 'Someone',
|
||||||
|
time: '5 minutes',
|
||||||
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
actual,
|
actual,
|
||||||
'Someone set the disappearing message timer to 5 minutes'
|
'Someone set the disappearing message time to 5 minutes.'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
import { Avatar } from './Avatar';
|
import { Avatar } from './Avatar';
|
||||||
import { MessageBody } from './conversation/MessageBody';
|
import { MessageBody } from './conversation/MessageBody';
|
||||||
|
@ -19,10 +20,10 @@ export type PropsData = {
|
||||||
name?: string;
|
name?: string;
|
||||||
type: 'group' | 'direct';
|
type: 'group' | 'direct';
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
isMe: boolean;
|
isMe?: boolean;
|
||||||
|
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
unreadCount: number;
|
unreadCount?: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
|
||||||
draftPreview?: string;
|
draftPreview?: string;
|
||||||
|
@ -80,7 +81,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
public renderUnread() {
|
public renderUnread() {
|
||||||
const { unreadCount } = this.props;
|
const { unreadCount } = this.props;
|
||||||
|
|
||||||
if (unreadCount > 0) {
|
if (isNumber(unreadCount) && unreadCount > 0) {
|
||||||
return (
|
return (
|
||||||
<div className="module-conversation-list-item__unread-count">
|
<div className="module-conversation-list-item__unread-count">
|
||||||
{unreadCount}
|
{unreadCount}
|
||||||
|
@ -103,12 +104,14 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
title,
|
title,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const withUnread = isNumber(unreadCount) && unreadCount > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-conversation-list-item__header">
|
<div className="module-conversation-list-item__header">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-list-item__header__name',
|
'module-conversation-list-item__header__name',
|
||||||
unreadCount > 0
|
withUnread
|
||||||
? 'module-conversation-list-item__header__name--with-unread'
|
? 'module-conversation-list-item__header__name--with-unread'
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
|
@ -128,7 +131,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-list-item__header__date',
|
'module-conversation-list-item__header__date',
|
||||||
unreadCount > 0
|
withUnread
|
||||||
? 'module-conversation-list-item__header__date--has-unread'
|
? 'module-conversation-list-item__header__date--has-unread'
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
|
@ -137,7 +140,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
timestamp={lastUpdated}
|
timestamp={lastUpdated}
|
||||||
extended={false}
|
extended={false}
|
||||||
module="module-conversation-list-item__header__timestamp"
|
module="module-conversation-list-item__header__timestamp"
|
||||||
withUnread={unreadCount > 0}
|
withUnread={withUnread}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,6 +161,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const withUnread = isNumber(unreadCount) && unreadCount > 0;
|
||||||
const showingDraft = shouldShowDraft && draftPreview;
|
const showingDraft = shouldShowDraft && draftPreview;
|
||||||
const deletedForEveryone = Boolean(
|
const deletedForEveryone = Boolean(
|
||||||
lastMessage && lastMessage.deletedForEveryone
|
lastMessage && lastMessage.deletedForEveryone
|
||||||
|
@ -178,7 +182,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
dir="auto"
|
dir="auto"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-list-item__message__text',
|
'module-conversation-list-item__message__text',
|
||||||
unreadCount > 0
|
withUnread
|
||||||
? 'module-conversation-list-item__message__text--has-unread'
|
? 'module-conversation-list-item__message__text--has-unread'
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
|
@ -219,6 +223,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { unreadCount, onClick, id, isSelected, style } = this.props;
|
const { unreadCount, onClick, id, isSelected, style } = this.props;
|
||||||
|
const withUnread = isNumber(unreadCount) && unreadCount > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -230,7 +235,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
style={style}
|
style={style}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-list-item',
|
'module-conversation-list-item',
|
||||||
unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
|
withUnread ? 'module-conversation-list-item--has-unread' : null,
|
||||||
isSelected ? 'module-conversation-list-item--is-selected' : null
|
isSelected ? 'module-conversation-list-item--is-selected' : null
|
||||||
)}
|
)}
|
||||||
data-id={cleanId(id)}
|
data-id={cleanId(id)}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#### No replacements
|
#### No replacements
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Intl id="leftTheGroup" i18n={util.i18n} />
|
<Intl id="deleteAndRestart" i18n={util.i18n} />
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Single string replacement
|
#### Single string replacement
|
||||||
|
@ -33,7 +33,10 @@
|
||||||
<Intl
|
<Intl
|
||||||
id="changedSinceVerified"
|
id="changedSinceVerified"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
components={['Alice', 'Bob']}
|
components={{
|
||||||
|
name1: 'Alice',
|
||||||
|
name2: 'Bob',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -43,19 +46,23 @@
|
||||||
<Intl
|
<Intl
|
||||||
id="changedSinceVerified"
|
id="changedSinceVerified"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
components={[
|
components={{
|
||||||
|
name1: (
|
||||||
<button
|
<button
|
||||||
key="external-1"
|
key="external-1"
|
||||||
style={{ backgroundColor: 'blue', color: 'white' }}
|
style={{ backgroundColor: 'blue', color: 'white' }}
|
||||||
>
|
>
|
||||||
Alice
|
Alice
|
||||||
</button>,
|
</button>
|
||||||
|
),
|
||||||
|
name2: (
|
||||||
<button
|
<button
|
||||||
key="external-2"
|
key="external-2"
|
||||||
style={{ backgroundColor: 'black', color: 'white' }}
|
style={{ backgroundColor: 'black', color: 'white' }}
|
||||||
>
|
>
|
||||||
Bob
|
Bob
|
||||||
</button>,
|
</button>
|
||||||
]}
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { LocalizerType, RenderTextCallbackType } from '../types/Util';
|
import { LocalizerType, RenderTextCallbackType } from '../types/Util';
|
||||||
|
import { ReplacementValuesType } from '../types/I18N';
|
||||||
|
|
||||||
export type FullJSXType = Array<JSX.Element | string> | JSX.Element | string;
|
export type FullJSXType = Array<JSX.Element | string> | JSX.Element | string;
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@ interface Props {
|
||||||
/** The translation string id */
|
/** The translation string id */
|
||||||
id: string;
|
id: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
components?: Array<FullJSXType>;
|
components?: Array<FullJSXType> | ReplacementValuesType<FullJSXType>;
|
||||||
renderText?: RenderTextCallbackType;
|
renderText?: RenderTextCallbackType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,13 +20,26 @@ export class Intl extends React.Component<Props> {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
public getComponent(index: number, key: number): FullJSXType | undefined {
|
public getComponent(
|
||||||
|
index: number,
|
||||||
|
placeholderName: string,
|
||||||
|
key: number
|
||||||
|
): FullJSXType | undefined {
|
||||||
const { id, components } = this.props;
|
const { id, components } = this.props;
|
||||||
|
|
||||||
|
if (!components) {
|
||||||
|
// tslint:disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`Error: Intl component prop not provided; Metadata: id '${id}', index ${index}, placeholder '${placeholderName}'`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(components)) {
|
||||||
if (!components || !components.length || components.length <= index) {
|
if (!components || !components.length || components.length <= index) {
|
||||||
// tslint:disable-next-line no-console
|
// tslint:disable-next-line no-console
|
||||||
console.log(
|
console.log(
|
||||||
`Error: Intl missing provided components for id ${id}, index ${index}`
|
`Error: Intl missing provided component for id '${id}', index ${index}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -34,12 +48,25 @@ export class Intl extends React.Component<Props> {
|
||||||
return <React.Fragment key={key}>{components[index]}</React.Fragment>;
|
return <React.Fragment key={key}>{components[index]}</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const value = components[placeholderName];
|
||||||
|
if (!value) {
|
||||||
|
// tslint:disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`Error: Intl missing provided component for id '${id}', placeholder '${placeholderName}'`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment key={key}>{value}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { id, i18n, renderText } = this.props;
|
const { components, id, i18n, renderText } = this.props;
|
||||||
|
|
||||||
const text = i18n(id);
|
const text = i18n(id);
|
||||||
const results: Array<any> = [];
|
const results: Array<any> = [];
|
||||||
const FIND_REPLACEMENTS = /\$[^$]+\$/g;
|
const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
|
||||||
|
|
||||||
// We have to do this, because renderText is not required in our Props object,
|
// We have to do this, because renderText is not required in our Props object,
|
||||||
// but it is always provided via defaultProps.
|
// but it is always provided via defaultProps.
|
||||||
|
@ -47,6 +74,12 @@ export class Intl extends React.Component<Props> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(components) && components.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
'Array syntax is not supported with more than one placeholder'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let componentIndex = 0;
|
let componentIndex = 0;
|
||||||
let key = 0;
|
let key = 0;
|
||||||
let lastTextIndex = 0;
|
let lastTextIndex = 0;
|
||||||
|
@ -63,7 +96,8 @@ export class Intl extends React.Component<Props> {
|
||||||
key += 1;
|
key += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(this.getComponent(componentIndex, key));
|
const placeholderName = match[1];
|
||||||
|
results.push(this.getComponent(componentIndex, placeholderName, key));
|
||||||
componentIndex += 1;
|
componentIndex += 1;
|
||||||
key += 1;
|
key += 1;
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ export interface PropsType {
|
||||||
|
|
||||||
// For display
|
// For display
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
isMe: boolean;
|
isMe?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
color?: ColorType;
|
color?: ColorType;
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
|
|
|
@ -16,6 +16,7 @@ const contactWithAllData = {
|
||||||
avatarPath: undefined,
|
avatarPath: undefined,
|
||||||
color: 'signal-blue',
|
color: 'signal-blue',
|
||||||
profileName: '-*Smartest Dude*-',
|
profileName: '-*Smartest Dude*-',
|
||||||
|
title: 'Rick Sanchez',
|
||||||
name: 'Rick Sanchez',
|
name: 'Rick Sanchez',
|
||||||
phoneNumber: '(305) 123-4567',
|
phoneNumber: '(305) 123-4567',
|
||||||
} as ConversationType;
|
} as ConversationType;
|
||||||
|
@ -23,6 +24,7 @@ const contactWithAllData = {
|
||||||
const contactWithJustProfile = {
|
const contactWithJustProfile = {
|
||||||
avatarPath: undefined,
|
avatarPath: undefined,
|
||||||
color: 'signal-blue',
|
color: 'signal-blue',
|
||||||
|
title: '-*Smartest Dude*-',
|
||||||
profileName: '-*Smartest Dude*-',
|
profileName: '-*Smartest Dude*-',
|
||||||
name: undefined,
|
name: undefined,
|
||||||
phoneNumber: '(305) 123-4567',
|
phoneNumber: '(305) 123-4567',
|
||||||
|
@ -33,6 +35,7 @@ const contactWithJustNumber = {
|
||||||
color: 'signal-blue',
|
color: 'signal-blue',
|
||||||
profileName: undefined,
|
profileName: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
|
title: '(305) 123-4567',
|
||||||
phoneNumber: '(305) 123-4567',
|
phoneNumber: '(305) 123-4567',
|
||||||
} as ConversationType;
|
} as ConversationType;
|
||||||
|
|
||||||
|
@ -43,6 +46,7 @@ const contactWithNothing = {
|
||||||
profileName: undefined,
|
profileName: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
phoneNumber: undefined,
|
phoneNumber: undefined,
|
||||||
|
title: 'Unknown contact',
|
||||||
} as ConversationType;
|
} as ConversationType;
|
||||||
|
|
||||||
storiesOf('Components/SafetyNumberChangeDialog', module)
|
storiesOf('Components/SafetyNumberChangeDialog', module)
|
||||||
|
|
|
@ -52,10 +52,7 @@ const SafetyDialogContents = ({
|
||||||
const shouldShowNumber = Boolean(contact.name || contact.profileName);
|
const shouldShowNumber = Boolean(contact.name || contact.profileName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li className="module-sfn-dialog__contact" key={contact.id}>
|
||||||
className="module-sfn-dialog__contact"
|
|
||||||
key={contact.phoneNumber}
|
|
||||||
>
|
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarPath={contact.avatarPath}
|
avatarPath={contact.avatarPath}
|
||||||
color={contact.color}
|
color={contact.color}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { storiesOf } from '@storybook/react';
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const contactWithAllData = {
|
const contactWithAllData = {
|
||||||
|
title: 'Summer Smith',
|
||||||
name: 'Summer Smith',
|
name: 'Summer Smith',
|
||||||
phoneNumber: '(305) 123-4567',
|
phoneNumber: '(305) 123-4567',
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
|
@ -22,6 +23,7 @@ const contactWithAllData = {
|
||||||
const contactWithJustProfile = {
|
const contactWithJustProfile = {
|
||||||
avatarPath: undefined,
|
avatarPath: undefined,
|
||||||
color: 'signal-blue',
|
color: 'signal-blue',
|
||||||
|
title: '-*Smartest Dude*-',
|
||||||
profileName: '-*Smartest Dude*-',
|
profileName: '-*Smartest Dude*-',
|
||||||
name: undefined,
|
name: undefined,
|
||||||
phoneNumber: '(305) 123-4567',
|
phoneNumber: '(305) 123-4567',
|
||||||
|
@ -32,6 +34,7 @@ const contactWithJustNumber = {
|
||||||
color: 'signal-blue',
|
color: 'signal-blue',
|
||||||
profileName: undefined,
|
profileName: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
|
title: '(305) 123-4567',
|
||||||
phoneNumber: '(305) 123-4567',
|
phoneNumber: '(305) 123-4567',
|
||||||
} as ConversationType;
|
} as ConversationType;
|
||||||
|
|
||||||
|
@ -40,6 +43,7 @@ const contactWithNothing = {
|
||||||
avatarPath: undefined,
|
avatarPath: undefined,
|
||||||
color: 'signal-blue',
|
color: 'signal-blue',
|
||||||
profileName: undefined,
|
profileName: undefined,
|
||||||
|
title: 'Unknown contact',
|
||||||
name: undefined,
|
name: undefined,
|
||||||
phoneNumber: undefined,
|
phoneNumber: undefined,
|
||||||
} as ConversationType;
|
} as ConversationType;
|
||||||
|
|
|
@ -36,10 +36,8 @@ export const SafetyNumberViewer = ({
|
||||||
const showNumber = Boolean(contact.name || contact.profileName);
|
const showNumber = Boolean(contact.name || contact.profileName);
|
||||||
const numberFragment = showNumber ? ` · ${contact.phoneNumber}` : '';
|
const numberFragment = showNumber ? ` · ${contact.phoneNumber}` : '';
|
||||||
const name = `${contact.title}${numberFragment}`;
|
const name = `${contact.title}${numberFragment}`;
|
||||||
const boldName = (key?: number) => (
|
const boldName = (
|
||||||
<span className="module-safety-number__bold-name" key={key}>
|
<span className="module-safety-number__bold-name">{name}</span>
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isVerified = contact.isVerified;
|
const isVerified = contact.isVerified;
|
||||||
|
@ -62,20 +60,23 @@ export const SafetyNumberViewer = ({
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={safetyNumberChangedKey}
|
id={safetyNumberChangedKey}
|
||||||
components={[boldName(1), boldName(2)]}
|
components={{
|
||||||
|
name1: boldName,
|
||||||
|
name2: boldName,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="module-safety-number__number">
|
<div className="module-safety-number__number">
|
||||||
{safetyNumber || getPlaceholder()}
|
{safetyNumber || getPlaceholder()}
|
||||||
</div>
|
</div>
|
||||||
<Intl i18n={i18n} id="verifyHelp" components={[boldName()]} />
|
<Intl i18n={i18n} id="verifyHelp" components={[boldName]} />
|
||||||
<div className="module-safety-number__verification-status">
|
<div className="module-safety-number__verification-status">
|
||||||
{isVerified ? (
|
{isVerified ? (
|
||||||
<span className="module-safety-number__icon--verified" />
|
<span className="module-safety-number__icon--verified" />
|
||||||
) : (
|
) : (
|
||||||
<span className="module-safety-number__icon--shield" />
|
<span className="module-safety-number__icon--shield" />
|
||||||
)}
|
)}
|
||||||
<Intl i18n={i18n} id={verifiedStatusKey} components={[boldName()]} />
|
<Intl i18n={i18n} id={verifiedStatusKey} components={[boldName]} />
|
||||||
</div>
|
</div>
|
||||||
<div className="module-safety-number__verify-container">
|
<div className="module-safety-number__verify-container">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -554,10 +554,12 @@ export class SearchResults extends React.Component<PropsType, StateType> {
|
||||||
<Intl
|
<Intl
|
||||||
id="noSearchResultsInConversation"
|
id="noSearchResultsInConversation"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
components={[
|
components={{
|
||||||
searchTerm,
|
searchTerm,
|
||||||
<Emojify key="item-1" text={searchConversationName} />,
|
conversationName: (
|
||||||
]}
|
<Emojify key="item-1" text={searchConversationName} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
i18n('noSearchResults', [searchTerm])
|
i18n('noSearchResults', [searchTerm])
|
||||||
|
|
|
@ -71,10 +71,10 @@ export const UpdateDialog = ({
|
||||||
<h3>{i18n('cannotUpdate')}</h3>
|
<h3>{i18n('cannotUpdate')}</h3>
|
||||||
<span>
|
<span>
|
||||||
<Intl
|
<Intl
|
||||||
components={[
|
components={{
|
||||||
<strong key="app">Signal.app</strong>,
|
app: <strong key="app">Signal.app</strong>,
|
||||||
<strong key="folder">/Applications</strong>,
|
folder: <strong key="folder">/Applications</strong>,
|
||||||
]}
|
}}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="readOnlyVolume"
|
id="readOnlyVolume"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Emojify } from './Emojify';
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { Emojify } from './Emojify';
|
||||||
|
|
||||||
export interface Props {
|
export interface PropsType {
|
||||||
title: string;
|
|
||||||
phoneNumber?: string;
|
|
||||||
name?: string;
|
|
||||||
profileName?: string;
|
|
||||||
module?: string;
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
title: string;
|
||||||
|
module?: string;
|
||||||
|
name?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
profileName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContactName extends React.Component<Props> {
|
export class ContactName extends React.Component<PropsType> {
|
||||||
public render() {
|
public render() {
|
||||||
const { module, title } = this.props;
|
const { module, title } = this.props;
|
||||||
const prefix = module ? module : 'module-contact-name';
|
const prefix = module ? module : 'module-contact-name';
|
||||||
|
|
|
@ -35,15 +35,46 @@ const renderMembershipRow = ({
|
||||||
</strong>
|
</strong>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if (firstThreeGroups.length >= 3) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={`ConversationHero--membership-${firstThreeGroups.length}`}
|
id="ConversationHero--membership-3"
|
||||||
components={firstThreeGroups}
|
components={{
|
||||||
|
group1: firstThreeGroups[0],
|
||||||
|
group2: firstThreeGroups[1],
|
||||||
|
group3: firstThreeGroups[2],
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (firstThreeGroups.length >= 2) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="ConversationHero--membership-2"
|
||||||
|
components={{
|
||||||
|
group1: firstThreeGroups[0],
|
||||||
|
group2: firstThreeGroups[1],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (firstThreeGroups.length >= 1) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="ConversationHero--membership-1"
|
||||||
|
components={{
|
||||||
|
group: firstThreeGroups[0],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -87,8 +118,6 @@ export const ConversationHero = ({
|
||||||
...groups.map(g => `g-${g}`),
|
...groups.map(g => `g-${g}`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const displayName =
|
|
||||||
name || (conversationType === 'group' ? i18n('unknownGroup') : undefined);
|
|
||||||
const phoneNumberOnly = Boolean(
|
const phoneNumberOnly = Boolean(
|
||||||
!name && !profileName && conversationType === 'direct'
|
!name && !profileName && conversationType === 'direct'
|
||||||
);
|
);
|
||||||
|
@ -113,7 +142,7 @@ export const ConversationHero = ({
|
||||||
) : (
|
) : (
|
||||||
<ContactName
|
<ContactName
|
||||||
title={title}
|
title={title}
|
||||||
name={displayName}
|
name={name}
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -17,6 +17,7 @@ const i18n = setupI18n('en', enMessages);
|
||||||
const getBaseProps = (isGroup = false): MessageRequestActionsProps => ({
|
const getBaseProps = (isGroup = false): MessageRequestActionsProps => ({
|
||||||
i18n,
|
i18n,
|
||||||
conversationType: isGroup ? 'group' : 'direct',
|
conversationType: isGroup ? 'group' : 'direct',
|
||||||
|
firstName: text('firstName', 'Cayce'),
|
||||||
title: isGroup
|
title: isGroup
|
||||||
? text('title', 'NYC Rock Climbers')
|
? text('title', 'NYC Rock Climbers')
|
||||||
: text('title', 'Cayce Bollard'),
|
: text('title', 'Cayce Bollard'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ContactName, Props as ContactNameProps } from './ContactName';
|
import { ContactName, PropsType as ContactNameProps } from './ContactName';
|
||||||
import {
|
import {
|
||||||
MessageRequestActionsConfirmation,
|
MessageRequestActionsConfirmation,
|
||||||
MessageRequestState,
|
MessageRequestState,
|
||||||
|
@ -11,6 +11,7 @@ import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
firstName?: string;
|
||||||
onAccept(): unknown;
|
onAccept(): unknown;
|
||||||
} & Omit<ContactNameProps, 'module' | 'i18n'> &
|
} & Omit<ContactNameProps, 'module' | 'i18n'> &
|
||||||
Omit<
|
Omit<
|
||||||
|
@ -20,18 +21,19 @@ export type Props = {
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
export const MessageRequestActions = ({
|
export const MessageRequestActions = ({
|
||||||
i18n,
|
|
||||||
name,
|
|
||||||
profileName,
|
|
||||||
phoneNumber,
|
|
||||||
title,
|
|
||||||
conversationType,
|
conversationType,
|
||||||
|
firstName,
|
||||||
|
i18n,
|
||||||
isBlocked,
|
isBlocked,
|
||||||
|
name,
|
||||||
|
onAccept,
|
||||||
onBlock,
|
onBlock,
|
||||||
onBlockAndDelete,
|
onBlockAndDelete,
|
||||||
onUnblock,
|
|
||||||
onDelete,
|
onDelete,
|
||||||
onAccept,
|
onUnblock,
|
||||||
|
phoneNumber,
|
||||||
|
profileName,
|
||||||
|
title,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||||
|
|
||||||
|
@ -69,7 +71,7 @@ export const MessageRequestActions = ({
|
||||||
name={name}
|
name={name}
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
title={title}
|
title={firstName || title}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
</strong>,
|
</strong>,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ContactName, Props as ContactNameProps } from './ContactName';
|
import { ContactName, PropsType as ContactNameProps } from './ContactName';
|
||||||
import { ConfirmationModal } from '../ConfirmationModal';
|
import { ConfirmationModal } from '../ConfirmationModal';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
@ -25,18 +25,18 @@ export type Props = {
|
||||||
|
|
||||||
// tslint:disable-next-line: max-func-body-length
|
// tslint:disable-next-line: max-func-body-length
|
||||||
export const MessageRequestActionsConfirmation = ({
|
export const MessageRequestActionsConfirmation = ({
|
||||||
|
conversationType,
|
||||||
i18n,
|
i18n,
|
||||||
name,
|
name,
|
||||||
profileName,
|
|
||||||
phoneNumber,
|
|
||||||
title,
|
|
||||||
conversationType,
|
|
||||||
onBlock,
|
onBlock,
|
||||||
onBlockAndDelete,
|
onBlockAndDelete,
|
||||||
onUnblock,
|
|
||||||
onDelete,
|
|
||||||
state,
|
|
||||||
onChangeState,
|
onChangeState,
|
||||||
|
onDelete,
|
||||||
|
onUnblock,
|
||||||
|
phoneNumber,
|
||||||
|
profileName,
|
||||||
|
state,
|
||||||
|
title,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
if (state === MessageRequestState.blocking) {
|
if (state === MessageRequestState.blocking) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../\_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/ProfileChangeNotification', module)
|
||||||
|
.add('From contact', () => {
|
||||||
|
return (
|
||||||
|
<ProfileChangeNotification
|
||||||
|
i18n={i18n}
|
||||||
|
changedContact={{
|
||||||
|
id: 'some-guid',
|
||||||
|
type: 'direct',
|
||||||
|
title: 'John',
|
||||||
|
name: 'John',
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
}}
|
||||||
|
change={{
|
||||||
|
type: 'name',
|
||||||
|
oldName: 'John Old',
|
||||||
|
newName: 'John New',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('From non-contact', () => {
|
||||||
|
return (
|
||||||
|
<ProfileChangeNotification
|
||||||
|
i18n={i18n}
|
||||||
|
changedContact={{
|
||||||
|
id: 'some-guid',
|
||||||
|
type: 'direct',
|
||||||
|
title: 'John',
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
}}
|
||||||
|
change={{
|
||||||
|
type: 'name',
|
||||||
|
oldName: 'John Old',
|
||||||
|
newName: 'John New',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
26
ts/components/conversation/ProfileChangeNotification.tsx
Normal file
26
ts/components/conversation/ProfileChangeNotification.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
import {
|
||||||
|
getStringForProfileChange,
|
||||||
|
ProfileNameChangeType,
|
||||||
|
} from '../../util/getStringForProfileChange';
|
||||||
|
|
||||||
|
export interface PropsType {
|
||||||
|
change: ProfileNameChangeType;
|
||||||
|
changedContact: ConversationType;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileChangeNotification(props: PropsType): JSX.Element {
|
||||||
|
const { change, changedContact, i18n } = props;
|
||||||
|
const message = getStringForProfileChange(change, changedContact, i18n);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-profile-change-notification">
|
||||||
|
<div className="module-profile-change-notification--icon" />
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -36,6 +36,10 @@ import {
|
||||||
PropsData as GroupNotificationProps,
|
PropsData as GroupNotificationProps,
|
||||||
} from './GroupNotification';
|
} from './GroupNotification';
|
||||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||||
|
import {
|
||||||
|
ProfileChangeNotification,
|
||||||
|
PropsType as ProfileChangeNotificationPropsType,
|
||||||
|
} from './ProfileChangeNotification';
|
||||||
|
|
||||||
type CallHistoryType = {
|
type CallHistoryType = {
|
||||||
type: 'callHistory';
|
type: 'callHistory';
|
||||||
|
@ -73,16 +77,22 @@ type ResetSessionNotificationType = {
|
||||||
type: 'resetSessionNotification';
|
type: 'resetSessionNotification';
|
||||||
data: null;
|
data: null;
|
||||||
};
|
};
|
||||||
|
type ProfileChangeNotificationType = {
|
||||||
|
type: 'profileChange';
|
||||||
|
data: ProfileChangeNotificationPropsType;
|
||||||
|
};
|
||||||
|
|
||||||
export type TimelineItemType =
|
export type TimelineItemType =
|
||||||
| CallHistoryType
|
| CallHistoryType
|
||||||
|
| GroupNotificationType
|
||||||
| LinkNotificationType
|
| LinkNotificationType
|
||||||
| MessageType
|
| MessageType
|
||||||
|
| ProfileChangeNotificationType
|
||||||
| ResetSessionNotificationType
|
| ResetSessionNotificationType
|
||||||
| SafetyNumberNotificationType
|
| SafetyNumberNotificationType
|
||||||
| TimerNotificationType
|
| TimerNotificationType
|
||||||
| UnsupportedMessageType
|
| UnsupportedMessageType
|
||||||
| VerificationNotificationType
|
| VerificationNotificationType;
|
||||||
| GroupNotificationType;
|
|
||||||
|
|
||||||
type PropsLocalType = {
|
type PropsLocalType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -159,6 +169,10 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
notification = (
|
notification = (
|
||||||
<ResetSessionNotification {...this.props} {...item.data} i18n={i18n} />
|
<ResetSessionNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
|
} else if (item.type === 'profileChange') {
|
||||||
|
notification = (
|
||||||
|
<ProfileChangeNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('TimelineItem: Unknown type!');
|
throw new Error('TimelineItem: Unknown type!');
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,8 @@ export class TimerNotification extends React.Component<Props> {
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={changeKey}
|
id={changeKey}
|
||||||
components={[
|
components={{
|
||||||
|
name: (
|
||||||
<ContactName
|
<ContactName
|
||||||
key="external-1"
|
key="external-1"
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
|
@ -51,9 +52,10 @@ export class TimerNotification extends React.Component<Props> {
|
||||||
title={title}
|
title={title}
|
||||||
name={name}
|
name={name}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>,
|
/>
|
||||||
timespan,
|
),
|
||||||
]}
|
time: timespan,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'fromMe':
|
case 'fromMe':
|
||||||
|
|
|
@ -248,6 +248,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we return null here, we hang up the call.
|
||||||
private async handleIncomingCall(call: Call): Promise<CallSettings | null> {
|
private async handleIncomingCall(call: Call): Promise<CallSettings | null> {
|
||||||
if (!this.uxActions || !this.localDeviceId) {
|
if (!this.uxActions || !this.localDeviceId) {
|
||||||
window.log.error('Missing required objects, ignoring incoming call.');
|
window.log.error('Missing required objects, ignoring incoming call.');
|
||||||
|
|
|
@ -27,6 +27,7 @@ export type ConversationType = {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
e164?: string;
|
e164?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
firstName?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color?: ColorType;
|
color?: ColorType;
|
||||||
|
@ -43,11 +44,11 @@ export type ConversationType = {
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
membersCount?: number;
|
membersCount?: number;
|
||||||
type: 'direct' | 'group';
|
type: 'direct' | 'group';
|
||||||
isMe: boolean;
|
isMe?: boolean;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
title: string;
|
title: string;
|
||||||
unreadCount: number;
|
unreadCount?: number;
|
||||||
isSelected: boolean;
|
isSelected?: boolean;
|
||||||
typingContact?: {
|
typingContact?: {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
|
|
@ -21,14 +21,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
avatarPath: conversation.avatarPath,
|
...conversation,
|
||||||
color: conversation.color,
|
|
||||||
conversationType: conversation.type,
|
conversationType: conversation.type,
|
||||||
isMe: conversation.isMe,
|
|
||||||
membersCount: conversation.membersCount,
|
|
||||||
name: conversation.name,
|
|
||||||
phoneNumber: conversation.phoneNumber,
|
|
||||||
profileName: conversation.profileName,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
IncomingMessage,
|
IncomingMessage,
|
||||||
MessageHistoryUnsyncedMessage,
|
MessageHistoryUnsyncedMessage,
|
||||||
OutgoingMessage,
|
OutgoingMessage,
|
||||||
|
ProfileChangeNotificationMessage,
|
||||||
VerifiedChangeMessage,
|
VerifiedChangeMessage,
|
||||||
} from '../../types/Message';
|
} from '../../types/Message';
|
||||||
|
|
||||||
|
@ -123,5 +124,29 @@ describe('Conversation', () => {
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('for profile change message', () => {
|
||||||
|
it('should update message but not timestamp (to prevent bump to top)', () => {
|
||||||
|
const input = {
|
||||||
|
currentTimestamp: 555,
|
||||||
|
lastMessage: {
|
||||||
|
type: 'profile-change',
|
||||||
|
conversationId: 'foo',
|
||||||
|
sent_at: 666,
|
||||||
|
timestamp: 666,
|
||||||
|
} as ProfileChangeNotificationMessage,
|
||||||
|
lastMessageNotificationText: 'John changed their profile name',
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
lastMessage: 'John changed their profile name',
|
||||||
|
lastMessageStatus: null,
|
||||||
|
lastMessageDeletedForEveryone: undefined,
|
||||||
|
timestamp: 555,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = Conversation.createLastMessageUpdate(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const createLastMessageUpdate = ({
|
||||||
|
|
||||||
const { type, expirationTimerUpdate, deletedForEveryone } = lastMessage;
|
const { type, expirationTimerUpdate, deletedForEveryone } = lastMessage;
|
||||||
const isMessageHistoryUnsynced = type === 'message-history-unsynced';
|
const isMessageHistoryUnsynced = type === 'message-history-unsynced';
|
||||||
|
const isProfileChangedMessage = type === 'profile-change';
|
||||||
const isVerifiedChangeMessage = type === 'verified-change';
|
const isVerifiedChangeMessage = type === 'verified-change';
|
||||||
const isExpireTimerUpdateFromSync = Boolean(
|
const isExpireTimerUpdateFromSync = Boolean(
|
||||||
expirationTimerUpdate && expirationTimerUpdate.fromSync
|
expirationTimerUpdate && expirationTimerUpdate.fromSync
|
||||||
|
@ -35,6 +36,7 @@ export const createLastMessageUpdate = ({
|
||||||
|
|
||||||
const shouldUpdateTimestamp = Boolean(
|
const shouldUpdateTimestamp = Boolean(
|
||||||
!isMessageHistoryUnsynced &&
|
!isMessageHistoryUnsynced &&
|
||||||
|
!isProfileChangedMessage &&
|
||||||
!isVerifiedChangeMessage &&
|
!isVerifiedChangeMessage &&
|
||||||
!isExpireTimerUpdateFromSync
|
!isExpireTimerUpdateFromSync
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,14 @@ export type LocaleMessagesType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReplacementValuesType<T> = {
|
||||||
|
[key: string]: T;
|
||||||
|
};
|
||||||
|
|
||||||
export type LocaleType = {
|
export type LocaleType = {
|
||||||
i18n: (key: string, placeholders: Array<string>) => string;
|
i18n: (
|
||||||
|
key: string,
|
||||||
|
placeholders: Array<string> | ReplacementValuesType<string>
|
||||||
|
) => string;
|
||||||
messages: LocaleMessagesType;
|
messages: LocaleMessagesType;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ export type Message = (
|
||||||
| UserMessage
|
| UserMessage
|
||||||
| VerifiedChangeMessage
|
| VerifiedChangeMessage
|
||||||
| MessageHistoryUnsyncedMessage
|
| MessageHistoryUnsyncedMessage
|
||||||
|
| ProfileChangeNotificationMessage
|
||||||
) & { deletedForEveryone?: boolean };
|
) & { deletedForEveryone?: boolean };
|
||||||
export type UserMessage = IncomingMessage | OutgoingMessage;
|
export type UserMessage = IncomingMessage | OutgoingMessage;
|
||||||
|
|
||||||
|
@ -77,6 +78,14 @@ export type MessageHistoryUnsyncedMessage = Readonly<
|
||||||
ExpirationTimerUpdate
|
ExpirationTimerUpdate
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ProfileChangeNotificationMessage = Readonly<
|
||||||
|
{
|
||||||
|
type: 'profile-change';
|
||||||
|
} & SharedMessageProperties &
|
||||||
|
MessageSchemaVersion5 &
|
||||||
|
ExpirationTimerUpdate
|
||||||
|
>;
|
||||||
|
|
||||||
type SharedMessageProperties = Readonly<{
|
type SharedMessageProperties = Readonly<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
sent_at: number;
|
sent_at: number;
|
||||||
|
|
|
@ -3,7 +3,14 @@ export type RenderTextCallbackType = (options: {
|
||||||
key: number;
|
key: number;
|
||||||
}) => JSX.Element | string;
|
}) => JSX.Element | string;
|
||||||
|
|
||||||
export type LocalizerType = (key: string, values?: Array<string>) => string;
|
export type ReplacementValuesType = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalizerType = (
|
||||||
|
key: string,
|
||||||
|
values?: Array<string> | ReplacementValuesType
|
||||||
|
) => string;
|
||||||
|
|
||||||
export type ColorType =
|
export type ColorType =
|
||||||
| 'red'
|
| 'red'
|
||||||
|
|
|
@ -19,6 +19,9 @@ export const initializeAttachmentMetadata = async (
|
||||||
if (message.type === 'message-history-unsynced') {
|
if (message.type === 'message-history-unsynced') {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
if (message.type === 'profile-change') {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
if (message.messageTimer || message.isViewOnce) {
|
if (message.messageTimer || message.isViewOnce) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -367,7 +367,10 @@ async function showFallbackReadOnlyDialog(
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
buttons: [locale.messages.ok.message],
|
buttons: [locale.messages.ok.message],
|
||||||
title: locale.messages.cannotUpdate.message,
|
title: locale.messages.cannotUpdate.message,
|
||||||
message: locale.i18n('readOnlyVolume', ['Signal.app', '/Applications']),
|
message: locale.i18n('readOnlyVolume', {
|
||||||
|
app: 'Signal.app',
|
||||||
|
folder: '/Applications',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
showingReadOnlyDialog = true;
|
showingReadOnlyDialog = true;
|
||||||
|
|
29
ts/util/getStringForProfileChange.ts
Normal file
29
ts/util/getStringForProfileChange.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
export type ProfileNameChangeType = {
|
||||||
|
type: 'name';
|
||||||
|
oldName: string;
|
||||||
|
newName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getStringForProfileChange(
|
||||||
|
change: ProfileNameChangeType,
|
||||||
|
changedContact: ConversationType,
|
||||||
|
i18n: LocalizerType
|
||||||
|
) {
|
||||||
|
if (change.type === 'name') {
|
||||||
|
return changedContact.name
|
||||||
|
? i18n('contactChangedProfileName', {
|
||||||
|
sender: changedContact.title,
|
||||||
|
oldProfile: change.oldName,
|
||||||
|
newProfile: change.newName,
|
||||||
|
})
|
||||||
|
: i18n('changedProfileName', {
|
||||||
|
oldProfile: change.oldName,
|
||||||
|
newProfile: change.newName,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('TimelineItem: Unknown type!');
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {
|
||||||
generateSecurityNumber,
|
generateSecurityNumber,
|
||||||
getPlaceholder as getSafetyNumberPlaceholder,
|
getPlaceholder as getSafetyNumberPlaceholder,
|
||||||
} from './safetyNumber';
|
} from './safetyNumber';
|
||||||
|
import { getStringForProfileChange } from './getStringForProfileChange';
|
||||||
import { hasExpired } from './hasExpired';
|
import { hasExpired } from './hasExpired';
|
||||||
import { isFileDangerous } from './isFileDangerous';
|
import { isFileDangerous } from './isFileDangerous';
|
||||||
import { makeLookup } from './makeLookup';
|
import { makeLookup } from './makeLookup';
|
||||||
|
@ -31,6 +32,7 @@ export {
|
||||||
eraseAllStorageServiceState,
|
eraseAllStorageServiceState,
|
||||||
generateSecurityNumber,
|
generateSecurityNumber,
|
||||||
getSafetyNumberPlaceholder,
|
getSafetyNumberPlaceholder,
|
||||||
|
getStringForProfileChange,
|
||||||
GoogleChrome,
|
GoogleChrome,
|
||||||
hasExpired,
|
hasExpired,
|
||||||
isFileDangerous,
|
isFileDangerous,
|
||||||
|
|
|
@ -207,7 +207,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/models/conversations.js",
|
||||||
"line": " await wrap(",
|
"line": " await wrap(",
|
||||||
"lineNumber": 664,
|
"lineNumber": 665,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-06-09T20:26:46.515Z"
|
"updated": "2020-06-09T20:26:46.515Z"
|
||||||
},
|
},
|
||||||
|
@ -243,6 +243,14 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2019-10-31T17:28:08.684Z"
|
"updated": "2019-10-31T17:28:08.684Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "js/modules/i18n.js",
|
||||||
|
"line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;",
|
||||||
|
"lineNumber": 44,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/modules/stickers.js",
|
"path": "js/modules/stickers.js",
|
||||||
|
@ -11416,6 +11424,14 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-04-13T23:38:26.065Z"
|
"updated": "2020-04-13T23:38:26.065Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "sticker-creator/util/i18n.tsx",
|
||||||
|
"line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;",
|
||||||
|
"lineNumber": 39,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "ts/backbone/views/Lightbox.js",
|
"path": "ts/backbone/views/Lightbox.js",
|
||||||
|
@ -11506,6 +11522,22 @@
|
||||||
"updated": "2020-06-03T19:23:21.195Z",
|
"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"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "ts/components/Intl.js",
|
||||||
|
"line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;",
|
||||||
|
"lineNumber": 35,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "ts/components/Intl.tsx",
|
||||||
|
"line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;",
|
||||||
|
"lineNumber": 69,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/LeftPane.js",
|
"path": "ts/components/LeftPane.js",
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
"function-name": [
|
"function-name": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
"function-regex": "^_?[a-z][\\w\\d]+$",
|
"function-regex": "^_?[A-Za-z][\\w\\d]+$",
|
||||||
"method-regex": "^_?[a-z][\\w\\d]+$",
|
"method-regex": "^_?[a-z][\\w\\d]+$",
|
||||||
"static-method-regex": "^_?[a-z][\\w\\d]+$"
|
"static-method-regex": "^_?[a-z][\\w\\d]+$"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue