Support for new GroupV2 groups
This commit is contained in:
parent
1ce0959fa1
commit
7a02cc815d
53 changed files with 7326 additions and 839 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,6 +13,7 @@ release/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
*.sublime*
|
*.sublime*
|
||||||
/sql/
|
/sql/
|
||||||
|
/start.sh
|
||||||
|
|
||||||
# generated files
|
# generated files
|
||||||
js/components.js
|
js/components.js
|
||||||
|
|
|
@ -18,6 +18,9 @@ example
|
||||||
coverage
|
coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|
||||||
|
# unneeded files
|
||||||
|
*.js.map
|
||||||
|
|
||||||
# build scripts
|
# build scripts
|
||||||
Makefile
|
Makefile
|
||||||
Gulpfile.js
|
Gulpfile.js
|
||||||
|
|
|
@ -272,7 +272,7 @@
|
||||||
"description": "Used as a label on a button allowing user to see more information"
|
"description": "Used as a label on a button allowing user to see more information"
|
||||||
},
|
},
|
||||||
"youLeftTheGroup": {
|
"youLeftTheGroup": {
|
||||||
"message": "You left the group.",
|
"message": "You are no longer a member of the group.",
|
||||||
"description": "Displayed when a user can't send a message because they have left the group"
|
"description": "Displayed when a user can't send a message because they have left the group"
|
||||||
},
|
},
|
||||||
"scrollDown": {
|
"scrollDown": {
|
||||||
|
@ -1381,6 +1381,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"timerSetByMember": {
|
||||||
|
"message": "A member set the disappearing message time to $time$.",
|
||||||
|
"description": "Message displayed when timer is by an unknown group member.",
|
||||||
|
"placeholders": {
|
||||||
|
"time": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "10m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"theyChangedTheTimer": {
|
"theyChangedTheTimer": {
|
||||||
"message": "$name$ set the disappearing message time 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.",
|
||||||
|
@ -1499,6 +1509,10 @@
|
||||||
"message": "Disappearing messages disabled",
|
"message": "Disappearing messages disabled",
|
||||||
"description": "Displayed in the left pane when the timer is turned off"
|
"description": "Displayed in the left pane when the timer is turned off"
|
||||||
},
|
},
|
||||||
|
"disappearingMessagesDisabledByMember": {
|
||||||
|
"message": "A member disabled disappearing messages.",
|
||||||
|
"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",
|
||||||
|
@ -2829,5 +2843,730 @@
|
||||||
"EmojiButton__label": {
|
"EmojiButton__label": {
|
||||||
"message": "Emoji",
|
"message": "Emoji",
|
||||||
"description": "Label for emoji button"
|
"description": "Label for emoji button"
|
||||||
|
},
|
||||||
|
"GroupV2--admin": {
|
||||||
|
"message": "Admin",
|
||||||
|
"description": "Shown next to the set of administrators in a group"
|
||||||
|
},
|
||||||
|
"GroupV2--timerConflict": {
|
||||||
|
"message": "Failed to update disappearing message timer. Please try again later.",
|
||||||
|
"description": "Shown if the user runs into a group update conflict attempting to update a GroupV2 message timer"
|
||||||
|
},
|
||||||
|
"GroupV2--title--change--other": {
|
||||||
|
"message": "$memberName$ changed the group name to \"$newTitle$\".",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"newTitle": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Saturday Hiking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--title--change--you": {
|
||||||
|
"message": "You changed the group name to \"$newTitle$\".",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"newTitle": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Saturday Hiking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--title--change--unknown": {
|
||||||
|
"message": "A member changed the group name to \"$newTitle$\".",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"newTitle": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Saturday Hiking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--title--remove--other": {
|
||||||
|
"message": "$memberName$ removed the group name.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--title--remove--you": {
|
||||||
|
"message": "You removed the group name.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--title--remove--unknown": {
|
||||||
|
"message": "A member removed the group name.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--avatar--change--other": {
|
||||||
|
"message": "$memberName$ changed the group avatar.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--avatar--change--you": {
|
||||||
|
"message": "You changed the group avatar.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--avatar--change--unknown": {
|
||||||
|
"message": "A member changed the group avatar.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--avatar--remove--other": {
|
||||||
|
"message": "$memberName$ removed the group avatar.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--avatar--remove--you": {
|
||||||
|
"message": "You removed the group avatar.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--avatar--remove--unknown": {
|
||||||
|
"message": "A member removed the group avatar.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--access-attributes--admins--other": {
|
||||||
|
"message": "$adminName$ changed who can edit group info to \"Only admins.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--access-attributes--admins--you": {
|
||||||
|
"message": "You changed who can edit group info to \"Only admins.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-attributes--admins--unknown": {
|
||||||
|
"message": "An admin changed who can edit group info to \"Only admins.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-attributes--all--other": {
|
||||||
|
"message": "$adminName$ changed who can edit group info to \"All members.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--access-attributes--all--you": {
|
||||||
|
"message": "You changed who can edit group info to \"All members.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-attributes--all--unknown": {
|
||||||
|
"message": "An admin changed who can edit group info to \"All members.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-members--admins--other": {
|
||||||
|
"message": "$adminName$ changed who can edit group membership to \"Only admins.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--access-members--admins--you": {
|
||||||
|
"message": "You changed who can edit group membership to \"Only admins.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-members--admins--unknown": {
|
||||||
|
"message": "An admin changed who can edit group membership to \"Only admins.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-members--all--other": {
|
||||||
|
"message": "$adminName$ changed who can edit group membership to \"All members.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--access-members--all--you": {
|
||||||
|
"message": "You changed who can edit group membership to \"All members.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--access-members--all--unknown": {
|
||||||
|
"message": "An admin changed who can edit group membership to \"All members.\"",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--member-add--from-invite--other": {
|
||||||
|
"message": "$inviteeName$ accepted an invitation to the group from $inviterName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Alice"
|
||||||
|
},
|
||||||
|
"inviterName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--from-invite--you": {
|
||||||
|
"message": "You accepted an invitation to the group from $inviterName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviterName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--from-invite--from-you": {
|
||||||
|
"message": "$inviteeName$ accepted your invitation to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--other--other": {
|
||||||
|
"message": "$adderName$ added $addeeName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adderName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"addeeName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--other--you": {
|
||||||
|
"message": "You added $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--other--unknown": {
|
||||||
|
"message": "A member added $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--you--other": {
|
||||||
|
"message": "$memberName$ added you to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--you--you": {
|
||||||
|
"message": "You joined the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--member-add--you--unknown": {
|
||||||
|
"message": "A member added you to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--other--other": {
|
||||||
|
"message": "$adminName$ removed $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--other--self": {
|
||||||
|
"message": "$memberName$ left.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--other--you": {
|
||||||
|
"message": "You removed $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--other--unknown": {
|
||||||
|
"message": "A member removed $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--you--other": {
|
||||||
|
"message": "$adminName$ removed you.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--you--you": {
|
||||||
|
"message": "You left.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--member-remove--you--unknown": {
|
||||||
|
"message": "A member removed you.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--member-privilege--promote--other--other": {
|
||||||
|
"message": "$adminName$ made $memberName$ an admin.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--promote--other--you": {
|
||||||
|
"message": "You made $memberName$ an admin.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--promote--other--unknown": {
|
||||||
|
"message": "An admin made $memberName$ an admin.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--promote--you--other": {
|
||||||
|
"message": "$adminName$ made you an admin.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--promote--you--unknown": {
|
||||||
|
"message": "An admin made you an admin.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--demote--other--other": {
|
||||||
|
"message": "$adminName$ revoked admin privileges from $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--demote--other--you": {
|
||||||
|
"message": "You revoked admin privileges from $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--demote--other--unknown": {
|
||||||
|
"message": "An admin revoked admin privileges from $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"memberName": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--demote--you--other": {
|
||||||
|
"message": "$adminName$ revoked your admin privileges.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--member-privilege--demote--you--unknown": {
|
||||||
|
"message": "An admin revoked your admin privileges.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--pending-add--one--other--other": {
|
||||||
|
"message": "$memberName$ invited 1 person to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--one--other--you": {
|
||||||
|
"message": "You invited $inviteeName$ to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--one--other--unknown": {
|
||||||
|
"message": "A member invited 1 person to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--one--you--other": {
|
||||||
|
"message": "$memberName$ invited you to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--one--you--unknown": {
|
||||||
|
"message": "A member invited you to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--many--other": {
|
||||||
|
"message": "$memberName$ invited $count$ people to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--many--you": {
|
||||||
|
"message": "You invited $count$ people to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-add--many--unknown": {
|
||||||
|
"message": "A member invited $count$ people to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"GroupV2--pending-remove--decline--other": {
|
||||||
|
"message": "1 person invited by $memberName$ declined the invitation to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--decline--you": {
|
||||||
|
"message": "$inviteeName$ declined your invitation to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--decline--unknown": {
|
||||||
|
"message": "1 person declined their invitation to the group.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke--one--other": {
|
||||||
|
"message": "$memberName$ revoked an invitation to the group for 1 person.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke--one--you": {
|
||||||
|
"message": "You revoked an invitation to the group for 1 person.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke--one--unknown": {
|
||||||
|
"message": "An admin revoked an invitation to the group for 1 person.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke--many--other": {
|
||||||
|
"message": "$memberName$ revoked invitations to the group for $count$ people.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke--many--you": {
|
||||||
|
"message": "You revoked invitations to the group for $count$ people.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke--many--unknown": {
|
||||||
|
"message": "An admin revoked invitations to the group for $count$ people.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from--one--other": {
|
||||||
|
"message": "$adminName$ revoked an invitation to the group for 1 person invited by $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from--one--you": {
|
||||||
|
"message": "You revoked an invitation to the group for 1 person invited by $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from--one--unknown": {
|
||||||
|
"message": "An admin revoked an invitation to the group for 1 person invited by $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"memberName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from-you--one--other": {
|
||||||
|
"message": "$adminName$ revoked the invitation to the group you sent to $inviteeName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from-you--one--you": {
|
||||||
|
"message": "You rescinded your invitation to $inviteeName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from-you--one--unknown": {
|
||||||
|
"message": "An admin revoked the invitation to the group you sent to $inviteeName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"inviteeName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from--many--other": {
|
||||||
|
"message": "$adminName$ revoked invitations to the group for $count$ people invited by $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from--many--you": {
|
||||||
|
"message": "You revoked invitations to the group for $count$ people invited by $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from--many--unknown": {
|
||||||
|
"message": "An admin revoked invitations to the group for $count$ people invited by $memberName$.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
},
|
||||||
|
"memberName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from-you--many--other": {
|
||||||
|
"message": "$adminName$ revoked the invitations to the group you sent to $count$ people.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"adminName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Bob"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from-you--many--you": {
|
||||||
|
"message": "You rescinded your invitation to $count$ people.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GroupV2--pending-remove--revoke-invite-from-you--many--unknown": {
|
||||||
|
"message": "An admin revoked the invitations to the group you sent to $count$ people.",
|
||||||
|
"description": "Shown in timeline or conversation preview when v2 group changes",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
426
js/background.js
426
js/background.js
|
@ -546,7 +546,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
window.isBeforeVersion(lastVersion, 'v1.35.0-beta.11') &&
|
window.isBeforeVersion(lastVersion, 'v1.36.0-beta.1') &&
|
||||||
window.isAfterVersion(lastVersion, 'v1.35.0-beta.1')
|
window.isAfterVersion(lastVersion, 'v1.35.0-beta.1')
|
||||||
) {
|
) {
|
||||||
await window.Signal.Services.eraseAllStorageServiceState();
|
await window.Signal.Services.eraseAllStorageServiceState();
|
||||||
|
@ -606,6 +606,17 @@
|
||||||
// flags are represented in the cached props we generate on load of each convo.
|
// flags are represented in the cached props we generate on load of each convo.
|
||||||
window.Signal.RemoteConfig.initRemoteConfig();
|
window.Signal.RemoteConfig.initRemoteConfig();
|
||||||
|
|
||||||
|
// On startup, we don't want to wait for the remote config fetch if we've already
|
||||||
|
// learned that this instance supports GroupsV2.
|
||||||
|
// This is how we keep it sticky. Once it is enabled, we never disable it.
|
||||||
|
if (
|
||||||
|
window.Signal.RemoteConfig.isEnabled('desktop.gv2') ||
|
||||||
|
window.storage.get('gv2-enabled')
|
||||||
|
) {
|
||||||
|
window.GV2 = true;
|
||||||
|
window.storage.put('gv2-enabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ConversationController.load(),
|
ConversationController.load(),
|
||||||
|
@ -1552,6 +1563,33 @@
|
||||||
removeMessageRequestListener();
|
removeMessageRequestListener();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Listen for changes to the `desktop.gv2` remote configuration flag
|
||||||
|
const removeGv2Listener = window.Signal.RemoteConfig.onChange(
|
||||||
|
'desktop.gv2',
|
||||||
|
async ({ enabled }) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.GV2 = true;
|
||||||
|
|
||||||
|
await window.storage.put('gv2-enabled', true);
|
||||||
|
|
||||||
|
window.Signal.Services.handleUnknownRecords(
|
||||||
|
window.textsecure.protobuf.ManifestRecord.Identifier.Type.GROUPV2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erase current manifest version so we re-process storage service data
|
||||||
|
await window.storage.remove('manifestVersion');
|
||||||
|
|
||||||
|
// Kick off storage service fetch to grab GroupV2 information
|
||||||
|
await window.Signal.Services.runStorageServiceSyncJob();
|
||||||
|
|
||||||
|
// This is a one-time thing
|
||||||
|
removeGv2Listener();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.getSyncRequest = () =>
|
window.getSyncRequest = () =>
|
||||||
|
@ -1657,45 +1695,58 @@
|
||||||
PASSWORD
|
PASSWORD
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
if (connectCount === 0) {
|
||||||
if (connectCount === 0) {
|
try {
|
||||||
const lonelyE164s = window
|
// Force a re-fetch before we process our queue. We may want to turn on something
|
||||||
.getConversations()
|
// which changes how we process incoming messages!
|
||||||
.filter(
|
await window.Signal.RemoteConfig.refreshRemoteConfig();
|
||||||
c =>
|
} catch (error) {
|
||||||
c.isPrivate() &&
|
window.log.error(
|
||||||
c.get('e164') &&
|
'connect: Error refreshing remote config:',
|
||||||
!c.get('uuid') &&
|
error && error.stack ? error.stack : error
|
||||||
!c.isEverUnregistered()
|
);
|
||||||
)
|
}
|
||||||
.map(c => c.get('e164'));
|
|
||||||
|
try {
|
||||||
if (lonelyE164s.length > 0) {
|
if (window.Signal.RemoteConfig.isEnabled('desktop.cds')) {
|
||||||
const lookup = await textsecure.messaging.getUuidsForE164s(
|
const lonelyE164s = window
|
||||||
lonelyE164s
|
.getConversations()
|
||||||
);
|
.filter(
|
||||||
const e164s = Object.keys(lookup);
|
c =>
|
||||||
e164s.forEach(e164 => {
|
c.isPrivate() &&
|
||||||
const uuid = lookup[e164];
|
c.get('e164') &&
|
||||||
if (!uuid) {
|
!c.get('uuid') &&
|
||||||
const byE164 = window.ConversationController.get(e164);
|
!c.isEverUnregistered()
|
||||||
if (byE164) {
|
)
|
||||||
byE164.setUnregistered();
|
.map(c => c.get('e164'));
|
||||||
}
|
|
||||||
}
|
if (lonelyE164s.length > 0) {
|
||||||
window.ConversationController.ensureContactIds({
|
const lookup = await textsecure.messaging.getUuidsForE164s(
|
||||||
e164,
|
lonelyE164s
|
||||||
uuid,
|
);
|
||||||
highTrust: true,
|
const e164s = Object.keys(lookup);
|
||||||
});
|
e164s.forEach(e164 => {
|
||||||
});
|
const uuid = lookup[e164];
|
||||||
}
|
if (!uuid) {
|
||||||
|
const byE164 = window.ConversationController.get(e164);
|
||||||
|
if (byE164) {
|
||||||
|
byE164.setUnregistered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.ConversationController.ensureContactIds({
|
||||||
|
e164,
|
||||||
|
uuid,
|
||||||
|
highTrust: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'connect: Error fetching UUIDs for lonely e164s:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'Error fetching UUIDs for lonely e164s:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectCount += 1;
|
connectCount += 1;
|
||||||
|
@ -1718,6 +1769,8 @@
|
||||||
);
|
);
|
||||||
window.textsecure.messageReceiver = messageReceiver;
|
window.textsecure.messageReceiver = messageReceiver;
|
||||||
|
|
||||||
|
window.Signal.Services.initializeGroupCredentialFetcher();
|
||||||
|
|
||||||
preMessageReceiverStatus = null;
|
preMessageReceiverStatus = null;
|
||||||
|
|
||||||
function addQueuedEventListener(name, handler) {
|
function addQueuedEventListener(name, handler) {
|
||||||
|
@ -1810,26 +1863,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: uncomment this once we want to start registering UUID support
|
const hasRegisteredGroupV2SupportKey = 'hasRegisteredGroupV2Support';
|
||||||
// const hasRegisteredUuidSupportKey = 'hasRegisteredUuidSupport';
|
if (
|
||||||
// if (
|
!storage.get(hasRegisteredGroupV2SupportKey) &&
|
||||||
// !storage.get(hasRegisteredUuidSupportKey) &&
|
textsecure.storage.user.getUuid()
|
||||||
// textsecure.storage.user.getUuid()
|
) {
|
||||||
// ) {
|
const server = WebAPI.connect({
|
||||||
// const server = WebAPI.connect({
|
username: USERNAME || OLD_USERNAME,
|
||||||
// username: USERNAME || OLD_USERNAME,
|
password: PASSWORD,
|
||||||
// password: PASSWORD,
|
});
|
||||||
// });
|
try {
|
||||||
// try {
|
await server.registerCapabilities({ gv2: true });
|
||||||
// await server.registerCapabilities({ uuid: true });
|
storage.put(hasRegisteredGroupV2SupportKey, true);
|
||||||
// storage.put(hasRegisteredUuidSupportKey, true);
|
} catch (error) {
|
||||||
// } catch (error) {
|
window.log.error(
|
||||||
// window.log.error(
|
'Error: Unable to register support for GroupV2.',
|
||||||
// 'Error: Unable to register support for UUID messages.',
|
error && error.stack ? error.stack : error
|
||||||
// error && error.stack ? error.stack : error
|
);
|
||||||
// );
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
const deviceId = textsecure.storage.user.getDeviceId();
|
const deviceId = textsecure.storage.user.getDeviceId();
|
||||||
|
|
||||||
|
@ -1918,6 +1970,49 @@
|
||||||
view.applyTheme();
|
view.applyTheme();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
// Note: once this function returns, there still might be messages being processed on
|
||||||
|
// a given conversation's queue. But we have processed all events from the websocket.
|
||||||
|
async function waitForEmptyEventQueue() {
|
||||||
|
if (!messageReceiver) {
|
||||||
|
window.log.info(
|
||||||
|
'waitForEmptyEventQueue: No messageReceiver available, returning early'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageReceiver.hasEmptied()) {
|
||||||
|
window.log.info(
|
||||||
|
'waitForEmptyEventQueue: Waiting for MessageReceiver empty event...'
|
||||||
|
);
|
||||||
|
let resolve;
|
||||||
|
let reject;
|
||||||
|
const promise = new Promise((innerResolve, innerReject) => {
|
||||||
|
resolve = innerResolve;
|
||||||
|
reject = innerReject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeout = setTimeout(reject, FIVE_MINUTES);
|
||||||
|
const onEmptyOnce = () => {
|
||||||
|
messageReceiver.removeEventListener('empty', onEmptyOnce);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
messageReceiver.addEventListener('empty', onEmptyOnce);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
'waitForEmptyEventQueue: Waiting for event handler queue idle...'
|
||||||
|
);
|
||||||
|
await eventHandlerQueue.onIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.waitForEmptyEventQueue = waitForEmptyEventQueue;
|
||||||
|
|
||||||
async function onEmpty() {
|
async function onEmpty() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
window.waitForAllBatchers(),
|
window.waitForAllBatchers(),
|
||||||
|
@ -1937,11 +2032,6 @@
|
||||||
logger: window.log,
|
logger: window.log,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force a re-fetch here when we've processed our queue. Without this, we won't try
|
|
||||||
// again for two hours after our first attempt. Which might have been while we were
|
|
||||||
// offline or didn't have credentials.
|
|
||||||
window.Signal.RemoteConfig.refreshRemoteConfig();
|
|
||||||
|
|
||||||
let interval = setInterval(() => {
|
let interval = setInterval(() => {
|
||||||
const view = window.owsDesktopApp.appView;
|
const view = window.owsDesktopApp.appView;
|
||||||
if (view) {
|
if (view) {
|
||||||
|
@ -2024,7 +2114,7 @@
|
||||||
// Note: this type of message is automatically removed from cache in MessageReceiver
|
// Note: this type of message is automatically removed from cache in MessageReceiver
|
||||||
|
|
||||||
const { typing, sender, senderUuid, senderDevice } = ev;
|
const { typing, sender, senderUuid, senderDevice } = ev;
|
||||||
const { groupId, started } = typing || {};
|
const { groupId, groupV2Id, started } = typing || {};
|
||||||
|
|
||||||
// We don't do anything with incoming typing messages if the setting is disabled
|
// We don't do anything with incoming typing messages if the setting is disabled
|
||||||
if (!storage.get('typingIndicators')) {
|
if (!storage.get('typingIndicators')) {
|
||||||
|
@ -2036,27 +2126,34 @@
|
||||||
uuid: senderUuid,
|
uuid: senderUuid,
|
||||||
highTrust: true,
|
highTrust: true,
|
||||||
});
|
});
|
||||||
const conversation = ConversationController.get(groupId || senderId);
|
const conversation = ConversationController.get(
|
||||||
|
groupV2Id || groupId || senderId
|
||||||
|
);
|
||||||
const ourId = ConversationController.getOurConversationId();
|
const ourId = ConversationController.getOurConversationId();
|
||||||
|
|
||||||
if (conversation) {
|
if (!conversation) {
|
||||||
// We drop typing notifications in groups we're not a part of
|
window.log.warn(
|
||||||
if (!conversation.isPrivate() && !conversation.hasMember(ourId)) {
|
`onTyping: Did not find conversation for typing indicator (groupv2(${groupV2Id}), group(${groupId}), ${sender}, ${senderUuid})`
|
||||||
window.log.warn(
|
);
|
||||||
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
return;
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
conversation.notifyTyping({
|
|
||||||
isTyping: started,
|
|
||||||
isMe: ourId === senderId,
|
|
||||||
sender,
|
|
||||||
senderUuid,
|
|
||||||
senderId,
|
|
||||||
senderDevice,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We drop typing notifications in groups we're not a part of
|
||||||
|
if (!conversation.isPrivate() && !conversation.hasMember(ourId)) {
|
||||||
|
window.log.warn(
|
||||||
|
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.notifyTyping({
|
||||||
|
isTyping: started,
|
||||||
|
isMe: ourId === senderId,
|
||||||
|
sender,
|
||||||
|
senderUuid,
|
||||||
|
senderId,
|
||||||
|
senderDevice,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onStickerPack(ev) {
|
async function onStickerPack(ev) {
|
||||||
|
@ -2227,6 +2324,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: this handler is only for v1 groups received via 'group sync' messages
|
||||||
async function onGroupReceived(ev) {
|
async function onGroupReceived(ev) {
|
||||||
const details = ev.groupDetails;
|
const details = ev.groupDetails;
|
||||||
const { id } = details;
|
const { id } = details;
|
||||||
|
@ -2244,6 +2342,13 @@
|
||||||
id,
|
id,
|
||||||
'group'
|
'group'
|
||||||
);
|
);
|
||||||
|
if (conversation.get('groupVersion') > 1) {
|
||||||
|
window.log.warn(
|
||||||
|
'Got group sync for v2 group: ',
|
||||||
|
conversation.idForLoggoing()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const memberConversations = details.membersE164.map(e164 =>
|
const memberConversations = details.membersE164.map(e164 =>
|
||||||
ConversationController.getOrCreate(e164, 'private')
|
ConversationController.getOrCreate(e164, 'private')
|
||||||
|
@ -2321,37 +2426,6 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptors
|
|
||||||
const getGroupDescriptor = group => ({
|
|
||||||
type: Message.GROUP,
|
|
||||||
id: group.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
|
||||||
const getDescriptorForSent = ({ message, destination, destinationUuid }) =>
|
|
||||||
message.group
|
|
||||||
? getGroupDescriptor(message.group)
|
|
||||||
: {
|
|
||||||
type: Message.PRIVATE,
|
|
||||||
id: ConversationController.ensureContactIds({
|
|
||||||
e164: destination,
|
|
||||||
uuid: destinationUuid,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
|
||||||
const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
|
|
||||||
message.group
|
|
||||||
? getGroupDescriptor(message.group)
|
|
||||||
: {
|
|
||||||
type: Message.PRIVATE,
|
|
||||||
id: ConversationController.ensureContactIds({
|
|
||||||
e164: source,
|
|
||||||
uuid: sourceUuid,
|
|
||||||
highTrust: true,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Received:
|
// Received:
|
||||||
async function handleMessageReceivedProfileUpdate({
|
async function handleMessageReceivedProfileUpdate({
|
||||||
data,
|
data,
|
||||||
|
@ -2369,6 +2443,50 @@
|
||||||
return confirm();
|
return confirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||||
|
const getDescriptorForReceived = ({ message, source, sourceUuid }) => {
|
||||||
|
if (message.groupV2) {
|
||||||
|
const { id } = message.groupV2;
|
||||||
|
const conversationId = ConversationController.ensureGroup(id, {
|
||||||
|
groupVersion: 2,
|
||||||
|
masterKey: message.groupV2.masterKey,
|
||||||
|
secretParams: message.groupV2.secretParams,
|
||||||
|
publicParams: message.groupV2.publicParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Message.GROUP,
|
||||||
|
id: conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.group) {
|
||||||
|
const { id } = message.group;
|
||||||
|
const fromContactId = ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversationId = ConversationController.ensureGroup(id, {
|
||||||
|
addedBy: fromContactId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Message.GROUP,
|
||||||
|
id: conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Message.PRIVATE,
|
||||||
|
id: ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Note: We do very little in this function, since everything in handleDataMessage is
|
// Note: We do very little in this function, since everything in handleDataMessage is
|
||||||
// inside a conversation-specific queue(). Any code here might run before an earlier
|
// inside a conversation-specific queue(). Any code here might run before an earlier
|
||||||
// message is processed in handleDataMessage().
|
// message is processed in handleDataMessage().
|
||||||
|
@ -2392,13 +2510,16 @@
|
||||||
|
|
||||||
if (data.message.reaction) {
|
if (data.message.reaction) {
|
||||||
const { reaction } = data.message;
|
const { reaction } = data.message;
|
||||||
window.log.info('Queuing reaction for', reaction.targetTimestamp);
|
window.log.info(
|
||||||
|
'Queuing incoming reaction for',
|
||||||
|
reaction.targetTimestamp
|
||||||
|
);
|
||||||
const reactionModel = Whisper.Reactions.add({
|
const reactionModel = Whisper.Reactions.add({
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
targetAuthorE164: reaction.targetAuthorE164,
|
targetAuthorE164: reaction.targetAuthorE164,
|
||||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||||
targetTimestamp: reaction.targetTimestamp.toNumber(),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
fromId: ConversationController.ensureContactIds({
|
fromId: ConversationController.ensureContactIds({
|
||||||
e164: data.source,
|
e164: data.source,
|
||||||
|
@ -2413,7 +2534,7 @@
|
||||||
|
|
||||||
if (data.message.delete) {
|
if (data.message.delete) {
|
||||||
const { delete: del } = data.message;
|
const { delete: del } = data.message;
|
||||||
window.log.info('Queuing DOE for', del.targetSentTimestamp);
|
window.log.info('Queuing incoming DOE for', del.targetSentTimestamp);
|
||||||
const deleteModel = Whisper.Deletes.add({
|
const deleteModel = Whisper.Deletes.add({
|
||||||
targetSentTimestamp: del.targetSentTimestamp,
|
targetSentTimestamp: del.targetSentTimestamp,
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
|
@ -2508,11 +2629,6 @@
|
||||||
data.unidentifiedDeliveries = unidentified.map(item => item.destination);
|
data.unidentifiedDeliveries = unidentified.map(item => item.destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGroup = descriptor.type === Message.GROUP;
|
|
||||||
const conversationId = isGroup
|
|
||||||
? ConversationController.ensureGroup(descriptor.id)
|
|
||||||
: descriptor.id;
|
|
||||||
|
|
||||||
return new Whisper.Message({
|
return new Whisper.Message({
|
||||||
source: textsecure.storage.user.getNumber(),
|
source: textsecure.storage.user.getNumber(),
|
||||||
sourceUuid: textsecure.storage.user.getUuid(),
|
sourceUuid: textsecure.storage.user.getUuid(),
|
||||||
|
@ -2521,7 +2637,7 @@
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
sent_to: sentTo,
|
sent_to: sentTo,
|
||||||
received_at: now,
|
received_at: now,
|
||||||
conversationId,
|
conversationId: descriptor.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent: true,
|
sent: true,
|
||||||
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
||||||
|
@ -2532,6 +2648,42 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||||
|
const getDescriptorForSent = ({ message, destination, destinationUuid }) => {
|
||||||
|
if (message.groupV2) {
|
||||||
|
const { id } = message.groupV2;
|
||||||
|
const conversationId = ConversationController.ensureGroup(id, {
|
||||||
|
groupVersion: 2,
|
||||||
|
masterKey: message.groupV2.masterKey,
|
||||||
|
secretParams: message.groupV2.secretParams,
|
||||||
|
publicParams: message.groupV2.publicParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Message.GROUP,
|
||||||
|
id: conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.group) {
|
||||||
|
const { id } = message.group;
|
||||||
|
const conversationId = ConversationController.ensureGroup(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Message.GROUP,
|
||||||
|
id: conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: Message.PRIVATE,
|
||||||
|
id: ConversationController.ensureContactIds({
|
||||||
|
e164: destination,
|
||||||
|
uuid: destinationUuid,
|
||||||
|
highTrust: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Note: We do very little in this function, since everything in handleDataMessage is
|
// Note: We do very little in this function, since everything in handleDataMessage is
|
||||||
// inside a conversation-specific queue(). Any code here might run before an earlier
|
// inside a conversation-specific queue(). Any code here might run before an earlier
|
||||||
// message is processed in handleDataMessage().
|
// message is processed in handleDataMessage().
|
||||||
|
@ -2555,12 +2707,13 @@
|
||||||
|
|
||||||
if (data.message.reaction) {
|
if (data.message.reaction) {
|
||||||
const { reaction } = data.message;
|
const { reaction } = data.message;
|
||||||
|
window.log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
||||||
const reactionModel = Whisper.Reactions.add({
|
const reactionModel = Whisper.Reactions.add({
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
targetAuthorE164: reaction.targetAuthorE164,
|
targetAuthorE164: reaction.targetAuthorE164,
|
||||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||||
targetTimestamp: reaction.targetTimestamp.toNumber(),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
fromId: ConversationController.getOurConversationId(),
|
fromId: ConversationController.getOurConversationId(),
|
||||||
fromSync: true,
|
fromSync: true,
|
||||||
|
@ -2574,6 +2727,7 @@
|
||||||
|
|
||||||
if (data.message.delete) {
|
if (data.message.delete) {
|
||||||
const { delete: del } = data.message;
|
const { delete: del } = data.message;
|
||||||
|
window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
|
||||||
const deleteModel = Whisper.Deletes.add({
|
const deleteModel = Whisper.Deletes.add({
|
||||||
targetSentTimestamp: del.targetSentTimestamp,
|
targetSentTimestamp: del.targetSentTimestamp,
|
||||||
serverTimestamp: del.serverTimestamp,
|
serverTimestamp: del.serverTimestamp,
|
||||||
|
@ -2594,20 +2748,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function initIncomingMessage(data, descriptor) {
|
function initIncomingMessage(data, descriptor) {
|
||||||
// Ensure that we have an accurate record for who this message is from
|
|
||||||
const fromContactId = ConversationController.ensureContactIds({
|
|
||||||
e164: data.source,
|
|
||||||
uuid: data.sourceUuid,
|
|
||||||
highTrust: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isGroup = descriptor.type === Message.GROUP;
|
|
||||||
const conversationId = isGroup
|
|
||||||
? ConversationController.ensureGroup(descriptor.id, {
|
|
||||||
addedBy: fromContactId,
|
|
||||||
})
|
|
||||||
: fromContactId;
|
|
||||||
|
|
||||||
return new Whisper.Message({
|
return new Whisper.Message({
|
||||||
source: data.source,
|
source: data.source,
|
||||||
sourceUuid: data.sourceUuid,
|
sourceUuid: data.sourceUuid,
|
||||||
|
@ -2615,7 +2755,7 @@
|
||||||
sent_at: data.timestamp,
|
sent_at: data.timestamp,
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
received_at: Date.now(),
|
received_at: Date.now(),
|
||||||
conversationId,
|
conversationId: descriptor.id,
|
||||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
unread: 1,
|
unread: 1,
|
||||||
|
@ -2729,6 +2869,14 @@
|
||||||
const conversationId = message.get('conversationId');
|
const conversationId = message.get('conversationId');
|
||||||
const conversation = ConversationController.get(conversationId);
|
const conversation = ConversationController.get(conversationId);
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.warn(
|
||||||
|
'onError: No conversation id, cannot save error bubble'
|
||||||
|
);
|
||||||
|
ev.confirm();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
// This matches the queueing behavior used in Message.handleDataMessage
|
// This matches the queueing behavior used in Message.handleDataMessage
|
||||||
conversation.queueJob(async () => {
|
conversation.queueJob(async () => {
|
||||||
const existingMessage = await window.Signal.Data.getMessageBySender(
|
const existingMessage = await window.Signal.Data.getMessageBySender(
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
if (conversation.isPrivate()) {
|
if (conversation.isPrivate()) {
|
||||||
recipients = [conversation.id];
|
recipients = [conversation.id];
|
||||||
} else {
|
} else {
|
||||||
recipients = conversation.get('members') || [];
|
recipients = conversation.getMemberIds();
|
||||||
}
|
}
|
||||||
const receipts = this.filter(
|
const receipts = this.filter(
|
||||||
receipt =>
|
receipt =>
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.log.info(`adding groupId(${groupId}) to blocked list`);
|
window.log.info(`adding group(${groupId}) to blocked list`);
|
||||||
storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId));
|
storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId));
|
||||||
};
|
};
|
||||||
storage.removeBlockedGroup = groupId => {
|
storage.removeBlockedGroup = groupId => {
|
||||||
|
|
|
@ -78,6 +78,9 @@
|
||||||
const e164 = this.get('e164');
|
const e164 = this.get('e164');
|
||||||
return `${uuid || e164} (${this.id})`;
|
return `${uuid || e164} (${this.id})`;
|
||||||
}
|
}
|
||||||
|
if (this.get('groupVersion') > 1) {
|
||||||
|
return `groupv2(${this.get('groupId')})`;
|
||||||
|
}
|
||||||
|
|
||||||
const groupId = this.get('groupId');
|
const groupId = this.get('groupId');
|
||||||
return `group(${groupId})`;
|
return `group(${groupId})`;
|
||||||
|
@ -403,24 +406,83 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchLatestGroupV2Data() {
|
||||||
|
if (this.get('groupVersion') !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.Signal.Groups.waitThenMaybeUpdateGroup({
|
||||||
|
conversation: this,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
maybeRepairGroupV2(data) {
|
||||||
|
if (
|
||||||
|
this.get('groupVersion') &&
|
||||||
|
this.get('masterKey') &&
|
||||||
|
this.get('secretParams') &&
|
||||||
|
this.get('publicParams')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(`Repairing GroupV2 conversation ${this.idForLogging()}`);
|
||||||
|
const { masterKey, secretParams, publicParams } = data;
|
||||||
|
|
||||||
|
this.set({ masterKey, secretParams, publicParams, groupVersion: 2 });
|
||||||
|
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
},
|
||||||
|
getGroupV2Info(groupChange) {
|
||||||
|
if (this.isPrivate() || this.get('groupVersion') !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
masterKey: window.Signal.Crypto.base64ToArrayBuffer(
|
||||||
|
this.get('masterKey')
|
||||||
|
),
|
||||||
|
revision: this.get('revision'),
|
||||||
|
members: this.getRecipients(),
|
||||||
|
groupChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getGroupV1Info() {
|
||||||
|
if (this.isPrivate() || this.get('groupVersion') > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.get('groupId'),
|
||||||
|
members: this.getRecipients(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
sendTypingMessage(isTyping) {
|
sendTypingMessage(isTyping) {
|
||||||
if (!textsecure.messaging) {
|
if (!textsecure.messaging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = !this.isPrivate() ? this.get('groupId') : null;
|
// We don't send typing messages to our other devices
|
||||||
const groupNumbers = this.getRecipients();
|
if (this.isMe()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const recipientId = this.isPrivate() ? this.getSendTarget() : null;
|
const recipientId = this.isPrivate() ? this.getSendTarget() : null;
|
||||||
|
const groupId = !this.isPrivate() ? this.get('groupId') : null;
|
||||||
|
const groupMembers = this.getRecipients();
|
||||||
|
|
||||||
|
// We don't send typing messages if our recipients list is empty
|
||||||
|
if (!this.isPrivate() && !groupMembers.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sendOptions = this.getSendOptions();
|
const sendOptions = this.getSendOptions();
|
||||||
|
|
||||||
this.wrapSend(
|
this.wrapSend(
|
||||||
textsecure.messaging.sendTypingMessage(
|
textsecure.messaging.sendTypingMessage(
|
||||||
{
|
{
|
||||||
isTyping,
|
isTyping,
|
||||||
recipientId,
|
recipientId,
|
||||||
groupId,
|
groupId,
|
||||||
groupNumbers,
|
groupMembers,
|
||||||
},
|
},
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
)
|
||||||
|
@ -581,7 +643,7 @@
|
||||||
lastUpdated: this.get('timestamp'),
|
lastUpdated: this.get('timestamp'),
|
||||||
membersCount: this.isPrivate()
|
membersCount: this.isPrivate()
|
||||||
? undefined
|
? undefined
|
||||||
: (this.get('members') || []).length,
|
: (this.get('membersV2') || this.get('members') || []).length,
|
||||||
messageRequestsEnabled,
|
messageRequestsEnabled,
|
||||||
muteExpiresAt: this.get('muteExpiresAt'),
|
muteExpiresAt: this.get('muteExpiresAt'),
|
||||||
name: this.get('name'),
|
name: this.get('name'),
|
||||||
|
@ -793,7 +855,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.fetchContacts();
|
this.fetchContacts();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.contactCollection.map(async contact => {
|
this.contactCollection.map(async contact => {
|
||||||
if (!contact.isMe()) {
|
if (!contact.isMe()) {
|
||||||
|
@ -1324,26 +1387,59 @@
|
||||||
return this.jobQueue.add(taskWithTimeout);
|
return this.jobQueue.add(taskWithTimeout);
|
||||||
},
|
},
|
||||||
|
|
||||||
getRecipients() {
|
getMembers() {
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
return [this.getSendTarget()];
|
return [this];
|
||||||
}
|
}
|
||||||
const me = ConversationController.getOurConversationId();
|
|
||||||
|
|
||||||
// The list of members might not always be conversationIds for old groups.
|
if (this.get('membersV2')) {
|
||||||
|
return _.compact(
|
||||||
|
this.get('membersV2').map(member => {
|
||||||
|
const c = ConversationController.get(member.conversationId);
|
||||||
|
|
||||||
|
// In groups we won't sent to contacts we believe are unregistered
|
||||||
|
if (c && c.isUnregistered()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get('members')) {
|
||||||
|
return _.compact(
|
||||||
|
this.get('members').map(id => {
|
||||||
|
const c = ConversationController.get(id);
|
||||||
|
|
||||||
|
// In groups we won't sent to contacts we believe are unregistered
|
||||||
|
if (c && c.isUnregistered()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
'getMembers: Group conversation had neither membersV2 nor members'
|
||||||
|
);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
getMemberIds() {
|
||||||
|
const members = this.getMembers();
|
||||||
|
return members.map(member => member.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecipients() {
|
||||||
|
const members = this.getMembers();
|
||||||
|
|
||||||
|
// Eliminate our
|
||||||
return _.compact(
|
return _.compact(
|
||||||
this.get('members').map(memberId => {
|
members.map(member => (member.isMe() ? null : member.getSendTarget()))
|
||||||
const c = ConversationController.get(memberId);
|
|
||||||
if (c.id === me) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// We don't want to even attempt a send if we have recently discovered that they
|
|
||||||
// are unregistered.
|
|
||||||
if (c.isUnregistered()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return c.getSendTarget();
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1549,11 +1645,11 @@
|
||||||
if (this.isMe()) {
|
if (this.isMe()) {
|
||||||
const dataMessage = await textsecure.messaging.getMessageProto(
|
const dataMessage = await textsecure.messaging.getMessageProto(
|
||||||
destination,
|
destination,
|
||||||
null,
|
null, // body
|
||||||
null,
|
null, // attachments
|
||||||
null,
|
null, // quote
|
||||||
null,
|
null, // preview
|
||||||
null,
|
null, // sticker
|
||||||
outgoingReaction,
|
outgoingReaction,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
|
@ -1568,11 +1664,11 @@
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
return textsecure.messaging.sendMessageToIdentifier(
|
return textsecure.messaging.sendMessageToIdentifier(
|
||||||
destination,
|
destination,
|
||||||
null,
|
null, // body
|
||||||
null,
|
null, // attachments
|
||||||
null,
|
null, // quote
|
||||||
null,
|
null, // preview
|
||||||
null,
|
null, // sticker
|
||||||
outgoingReaction,
|
outgoingReaction,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
|
@ -1582,17 +1678,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return textsecure.messaging.sendMessageToGroup(
|
return textsecure.messaging.sendMessageToGroup(
|
||||||
this.get('groupId'),
|
{
|
||||||
this.getRecipients(),
|
groupV1: this.getGroupV1Info(),
|
||||||
null,
|
groupV2: this.getGroupV2Info(),
|
||||||
null,
|
reaction: outgoingReaction,
|
||||||
null,
|
timestamp,
|
||||||
null,
|
expireTimer,
|
||||||
null,
|
profileKey,
|
||||||
outgoingReaction,
|
},
|
||||||
timestamp,
|
|
||||||
expireTimer,
|
|
||||||
profileKey,
|
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
@ -1741,7 +1834,7 @@
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
sticker,
|
sticker,
|
||||||
null,
|
null, // reaction
|
||||||
now,
|
now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey
|
profileKey
|
||||||
|
@ -1752,43 +1845,38 @@
|
||||||
const conversationType = this.get('type');
|
const conversationType = this.get('type');
|
||||||
const options = this.getSendOptions();
|
const options = this.getSendOptions();
|
||||||
|
|
||||||
const promise = (() => {
|
let promise;
|
||||||
switch (conversationType) {
|
if (conversationType === Message.GROUP) {
|
||||||
case Message.PRIVATE:
|
promise = textsecure.messaging.sendMessageToGroup(
|
||||||
return textsecure.messaging.sendMessageToIdentifier(
|
{
|
||||||
destination,
|
attachments: finalAttachments,
|
||||||
messageBody,
|
expireTimer,
|
||||||
finalAttachments,
|
groupV1: this.getGroupV1Info(),
|
||||||
quote,
|
groupV2: this.getGroupV2Info(),
|
||||||
preview,
|
messageText: messageBody,
|
||||||
sticker,
|
preview,
|
||||||
null,
|
profileKey,
|
||||||
now,
|
quote,
|
||||||
expireTimer,
|
sticker,
|
||||||
profileKey,
|
timestamp: now,
|
||||||
options
|
},
|
||||||
);
|
options
|
||||||
case Message.GROUP:
|
);
|
||||||
return textsecure.messaging.sendMessageToGroup(
|
} else {
|
||||||
this.get('groupId'),
|
promise = textsecure.messaging.sendMessageToIdentifier(
|
||||||
this.getRecipients(),
|
destination,
|
||||||
messageBody,
|
messageBody,
|
||||||
finalAttachments,
|
finalAttachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
sticker,
|
sticker,
|
||||||
null,
|
null, // reaction
|
||||||
now,
|
now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
default:
|
}
|
||||||
throw new TypeError(
|
|
||||||
`Invalid conversation type: '${conversationType}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return message.send(this.wrapSend(promise));
|
return message.send(this.wrapSend(promise));
|
||||||
});
|
});
|
||||||
|
@ -2012,7 +2100,9 @@
|
||||||
|
|
||||||
const currentTimestamp = this.get('timestamp') || null;
|
const currentTimestamp = this.get('timestamp') || null;
|
||||||
const timestamp = activityMessage
|
const timestamp = activityMessage
|
||||||
? activityMessage.get('sent_at') || currentTimestamp
|
? activityMessage.get('sent_at') ||
|
||||||
|
activityMessage.get('received_at') ||
|
||||||
|
currentTimestamp
|
||||||
: currentTimestamp;
|
: currentTimestamp;
|
||||||
|
|
||||||
this.set({
|
this.set({
|
||||||
|
@ -2043,12 +2133,90 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateExpirationTimerInGroupV2(seconds) {
|
||||||
|
// Make change on the server
|
||||||
|
const actions = window.Signal.Groups.buildDisappearingMessagesTimerChange(
|
||||||
|
{
|
||||||
|
expireTimer: seconds || 0,
|
||||||
|
group: this.attributes,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let signedGroupChange;
|
||||||
|
try {
|
||||||
|
signedGroupChange = await window.Signal.Groups.uploadGroupChange({
|
||||||
|
actions,
|
||||||
|
group: this.attributes,
|
||||||
|
serverPublicParamsBase64: window.getServerPublicParams(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Get latest GroupV2 data, since we ran into trouble updating it
|
||||||
|
this.fetchLatestGroupV2Data();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local conversation
|
||||||
|
this.set({
|
||||||
|
expireTimer: seconds || 0,
|
||||||
|
revision: actions.version,
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
|
// Create local notification
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const id = window.getGuid();
|
||||||
|
const message = MessageController.register(
|
||||||
|
id,
|
||||||
|
new Whisper.Message({
|
||||||
|
id,
|
||||||
|
conversationId: this.id,
|
||||||
|
sent_at: timestamp,
|
||||||
|
received_at: timestamp,
|
||||||
|
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
|
expirationTimerUpdate: {
|
||||||
|
expireTimer: seconds,
|
||||||
|
sourceUuid: this.ourUuid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
|
Message: Whisper.Message,
|
||||||
|
forceSave: true,
|
||||||
|
});
|
||||||
|
this.trigger('newmessage', message);
|
||||||
|
|
||||||
|
// Send message to all group members
|
||||||
|
const profileKey = this.get('profileSharing')
|
||||||
|
? storage.get('profileKey')
|
||||||
|
: undefined;
|
||||||
|
const sendOptions = this.getSendOptions();
|
||||||
|
const promise = textsecure.messaging.sendMessageToGroup(
|
||||||
|
{
|
||||||
|
groupV2: this.getGroupV2Info(signedGroupChange.toArrayBuffer()),
|
||||||
|
timestamp,
|
||||||
|
profileKey,
|
||||||
|
},
|
||||||
|
sendOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
message.send(promise);
|
||||||
|
},
|
||||||
|
|
||||||
async updateExpirationTimer(
|
async updateExpirationTimer(
|
||||||
providedExpireTimer,
|
providedExpireTimer,
|
||||||
providedSource,
|
providedSource,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
options = {}
|
options = {}
|
||||||
) {
|
) {
|
||||||
|
if (this.get('groupVersion') === 2) {
|
||||||
|
if (providedSource || receivedAt) {
|
||||||
|
throw new Error(
|
||||||
|
'updateExpirationTimer: GroupV2 timers are not updated this way'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.updateExpirationTimerInGroupV2(providedExpireTimer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let expireTimer = providedExpireTimer;
|
let expireTimer = providedExpireTimer;
|
||||||
let source = providedSource;
|
let source = providedSource;
|
||||||
if (this.get('left')) {
|
if (this.get('left')) {
|
||||||
|
@ -2131,12 +2299,12 @@
|
||||||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||||
const dataMessage = await textsecure.messaging.getMessageProto(
|
const dataMessage = await textsecure.messaging.getMessageProto(
|
||||||
this.getSendTarget(),
|
this.getSendTarget(),
|
||||||
null,
|
null, // body
|
||||||
[],
|
[], // attachments
|
||||||
null,
|
null, // quote
|
||||||
[],
|
[], // preview
|
||||||
null,
|
null, // sticker
|
||||||
null,
|
null, // reaction
|
||||||
message.get('sent_at'),
|
message.get('sent_at'),
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -2250,79 +2418,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateGroup(providedGroupUpdate) {
|
|
||||||
let groupUpdate = providedGroupUpdate;
|
|
||||||
|
|
||||||
if (this.isPrivate()) {
|
|
||||||
throw new Error('Called update group on private conversation');
|
|
||||||
}
|
|
||||||
if (groupUpdate === undefined) {
|
|
||||||
groupUpdate = this.pick(['name', 'avatar', 'members']);
|
|
||||||
}
|
|
||||||
const now = Date.now();
|
|
||||||
const model = new Whisper.Message({
|
|
||||||
conversationId: this.id,
|
|
||||||
type: 'outgoing',
|
|
||||||
sent_at: now,
|
|
||||||
received_at: now,
|
|
||||||
group_update: groupUpdate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
|
|
||||||
model.set({ id });
|
|
||||||
|
|
||||||
const message = MessageController.register(model.id, model);
|
|
||||||
this.addSingleMessage(message);
|
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
|
||||||
message.send(
|
|
||||||
this.wrapSend(
|
|
||||||
textsecure.messaging.updateGroup(
|
|
||||||
this.id,
|
|
||||||
this.get('name'),
|
|
||||||
this.get('avatar'),
|
|
||||||
this.get('members'),
|
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async leaveGroup() {
|
|
||||||
const now = Date.now();
|
|
||||||
if (this.get('type') === 'group') {
|
|
||||||
const groupIdentifiers = this.getRecipients();
|
|
||||||
this.set({ left: true });
|
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
|
||||||
|
|
||||||
const model = new Whisper.Message({
|
|
||||||
group_update: { left: 'You' },
|
|
||||||
conversationId: this.id,
|
|
||||||
type: 'outgoing',
|
|
||||||
sent_at: now,
|
|
||||||
received_at: now,
|
|
||||||
});
|
|
||||||
|
|
||||||
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
model.set({ id });
|
|
||||||
|
|
||||||
const message = MessageController.register(model.id, model);
|
|
||||||
this.addSingleMessage(message);
|
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
|
||||||
message.send(
|
|
||||||
this.wrapSend(
|
|
||||||
textsecure.messaging.leaveGroup(this.id, groupIdentifiers, options)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async markRead(newestUnreadDate, providedOptions) {
|
async markRead(newestUnreadDate, providedOptions) {
|
||||||
const options = providedOptions || {};
|
const options = providedOptions || {};
|
||||||
_.defaults(options, { sendReadReceipts: true });
|
_.defaults(options, { sendReadReceipts: true });
|
||||||
|
@ -2444,14 +2539,7 @@
|
||||||
|
|
||||||
getProfiles() {
|
getProfiles() {
|
||||||
// request all conversation members' keys
|
// request all conversation members' keys
|
||||||
let conversations = [];
|
const conversations = this.getMembers();
|
||||||
if (this.isPrivate()) {
|
|
||||||
conversations = [this];
|
|
||||||
} else {
|
|
||||||
conversations = this.get('members')
|
|
||||||
.map(id => ConversationController.get(id))
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
_.map(conversations, conversation => {
|
_.map(conversations, conversation => {
|
||||||
this.getProfile(conversation.get('uuid'), conversation.get('e164'));
|
this.getProfile(conversation.get('uuid'), conversation.get('e164'));
|
||||||
|
@ -2822,30 +2910,21 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
hasMember(identifier) {
|
hasMember(identifier) {
|
||||||
const cid = ConversationController.getConversationId(identifier);
|
const id = ConversationController.getConversationId(identifier);
|
||||||
return cid && _.contains(this.get('members'), cid);
|
const memberIds = this.getMemberIds();
|
||||||
|
|
||||||
|
return _.contains(memberIds, id);
|
||||||
},
|
},
|
||||||
fetchContacts() {
|
fetchContacts() {
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
this.contactCollection.reset([this]);
|
this.contactCollection.reset([this]);
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
const members = this.get('members') || [];
|
const members = this.getMembers();
|
||||||
const promises = members.map(identifier =>
|
_.forEach(members, member => {
|
||||||
ConversationController.getOrCreateAndWait(identifier, 'private')
|
this.listenTo(member, 'change:verified', this.onMemberVerifiedChange);
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.all(promises).then(contacts => {
|
|
||||||
_.forEach(contacts, contact => {
|
|
||||||
this.listenTo(
|
|
||||||
contact,
|
|
||||||
'change:verified',
|
|
||||||
this.onMemberVerifiedChange
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.contactCollection.reset(contacts);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.contactCollection.reset(members);
|
||||||
},
|
},
|
||||||
|
|
||||||
async destroyMessages() {
|
async destroyMessages() {
|
||||||
|
@ -2946,6 +3025,43 @@
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
canChangeTimer() {
|
||||||
|
if (this.isPrivate()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get('groupVersion') !== 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessControlEnum =
|
||||||
|
textsecure.protobuf.AccessControl.AccessRequired;
|
||||||
|
const accessControl = this.get('accessControl');
|
||||||
|
const canAnyoneChangeTimer =
|
||||||
|
accessControl &&
|
||||||
|
(accessControl.attributes === accessControlEnum.ANY ||
|
||||||
|
accessControl.attributes === accessControlEnum.MEMBER);
|
||||||
|
if (canAnyoneChangeTimer) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberEnum = textsecure.protobuf.Member.Role;
|
||||||
|
const members = this.get('membersV2') || [];
|
||||||
|
const myId = ConversationController.getConversationId(
|
||||||
|
textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
|
||||||
|
);
|
||||||
|
const me = members.find(item => item.conversationId === myId);
|
||||||
|
if (!me) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdministrator = me.role === memberEnum.ADMINISTRATOR;
|
||||||
|
if (isAdministrator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
// Set of items to captureChanges on:
|
// Set of items to captureChanges on:
|
||||||
// [-] uuid
|
// [-] uuid
|
||||||
|
@ -3184,4 +3300,71 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
||||||
|
|
||||||
|
// This is a wrapper model used to display group members in the member list view, within
|
||||||
|
// the world of backbone, but layering another bit of group-specific data top of base
|
||||||
|
// conversation data.
|
||||||
|
Whisper.GroupMemberConversation = Backbone.Model.extend({
|
||||||
|
initialize(attributes) {
|
||||||
|
const { conversation, isAdmin } = attributes;
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(
|
||||||
|
'GroupMemberConversation.initialze: conversation required!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!_.isBoolean(isAdmin)) {
|
||||||
|
throw new Error('GroupMemberConversation.initialze: isAdmin required!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our underlying conversation changes, we change too
|
||||||
|
this.listenTo(conversation, 'change', () => {
|
||||||
|
this.trigger('change', this);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.conversation = conversation;
|
||||||
|
this.isAdmin = isAdmin;
|
||||||
|
},
|
||||||
|
|
||||||
|
format() {
|
||||||
|
return {
|
||||||
|
...this.conversation.format(),
|
||||||
|
isAdmin: this.isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
get(...params) {
|
||||||
|
return this.conversation.get(...params);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
return this.conversation.getTitle();
|
||||||
|
},
|
||||||
|
|
||||||
|
isMe() {
|
||||||
|
return this.conversation.isMe();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// We need a custom collection here to get the sorting we need
|
||||||
|
Whisper.GroupConversationCollection = Backbone.Collection.extend({
|
||||||
|
model: Whisper.GroupMemberConversation,
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.collator = new Intl.Collator();
|
||||||
|
},
|
||||||
|
|
||||||
|
comparator(left, right) {
|
||||||
|
if (left.isAdmin && !right.isAdmin) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!left.isAdmin && right.isAdmin) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftLower = left.getTitle().toLowerCase();
|
||||||
|
const rightLower = right.getTitle().toLowerCase();
|
||||||
|
return this.collator.compare(leftLower, rightLower);
|
||||||
|
},
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -45,6 +45,9 @@
|
||||||
|
|
||||||
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
|
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
|
||||||
const { bytesFromString } = window.Signal.Crypto;
|
const { bytesFromString } = window.Signal.Crypto;
|
||||||
|
const PLACEHOLDER_CONTACT = {
|
||||||
|
title: i18n('unknownContact'),
|
||||||
|
};
|
||||||
|
|
||||||
window.AccountCache = Object.create(null);
|
window.AccountCache = Object.create(null);
|
||||||
window.AccountJobs = Object.create(null);
|
window.AccountJobs = Object.create(null);
|
||||||
|
@ -140,6 +143,7 @@
|
||||||
!this.isEndSession() &&
|
!this.isEndSession() &&
|
||||||
!this.isExpirationTimerUpdate() &&
|
!this.isExpirationTimerUpdate() &&
|
||||||
!this.isGroupUpdate() &&
|
!this.isGroupUpdate() &&
|
||||||
|
!this.isGroupV2Change() &&
|
||||||
!this.isKeyChange() &&
|
!this.isKeyChange() &&
|
||||||
!this.isMessageHistoryUnsynced() &&
|
!this.isMessageHistoryUnsynced() &&
|
||||||
!this.isProfileChange() &&
|
!this.isProfileChange() &&
|
||||||
|
@ -156,6 +160,12 @@
|
||||||
data: this.getPropsForUnsupportedMessage(),
|
data: this.getPropsForUnsupportedMessage(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (this.isGroupV2Change()) {
|
||||||
|
return {
|
||||||
|
type: 'groupV2Change',
|
||||||
|
data: this.getPropsForGroupV2Change(),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (this.isMessageHistoryUnsynced()) {
|
if (this.isMessageHistoryUnsynced()) {
|
||||||
return {
|
return {
|
||||||
type: 'linkNotification',
|
type: 'linkNotification',
|
||||||
|
@ -213,24 +223,13 @@
|
||||||
|
|
||||||
// Other top-level prop-generation
|
// Other top-level prop-generation
|
||||||
getPropsForSearchResult() {
|
getPropsForSearchResult() {
|
||||||
const ourId = ConversationController.getOurConversationId();
|
|
||||||
const sourceId = this.getContactId();
|
const sourceId = this.getContactId();
|
||||||
const fromContact = this.findAndFormatContact(sourceId);
|
const from = this.findAndFormatContact(sourceId);
|
||||||
|
|
||||||
if (ourId === sourceId) {
|
|
||||||
fromContact.isMe = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const convo = this.getConversation();
|
const convo = this.getConversation();
|
||||||
|
const to = this.findAndFormatContact(convo.get('id'));
|
||||||
const to = convo ? this.findAndFormatContact(convo.get('id')) : {};
|
|
||||||
|
|
||||||
if (to && convo && convo.isMe()) {
|
|
||||||
to.isMe = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: fromContact || {},
|
from,
|
||||||
to,
|
to,
|
||||||
|
|
||||||
isSelected: this.isSelected,
|
isSelected: this.isSelected,
|
||||||
|
@ -358,6 +357,9 @@
|
||||||
versionAtReceive < requiredVersion
|
versionAtReceive < requiredVersion
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isGroupV2Change() {
|
||||||
|
return Boolean(this.get('groupV2Change'));
|
||||||
|
},
|
||||||
isExpirationTimerUpdate() {
|
isExpirationTimerUpdate() {
|
||||||
const flag =
|
const flag =
|
||||||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||||
|
@ -399,6 +401,16 @@
|
||||||
contact: this.findAndFormatContact(sourceId),
|
contact: this.findAndFormatContact(sourceId),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getPropsForGroupV2Change() {
|
||||||
|
const { protobuf } = window.textsecure;
|
||||||
|
|
||||||
|
return {
|
||||||
|
AccessControlEnum: protobuf.AccessControl.AccessRequired,
|
||||||
|
RoleEnum: protobuf.Member.Role,
|
||||||
|
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||||
|
change: this.get('groupV2Change'),
|
||||||
|
};
|
||||||
|
},
|
||||||
getPropsForTimerNotification() {
|
getPropsForTimerNotification() {
|
||||||
const timerUpdate = this.get('expirationTimerUpdate');
|
const timerUpdate = this.get('expirationTimerUpdate');
|
||||||
if (!timerUpdate) {
|
if (!timerUpdate) {
|
||||||
|
@ -414,9 +426,10 @@
|
||||||
uuid: sourceUuid,
|
uuid: sourceUuid,
|
||||||
});
|
});
|
||||||
const ourId = ConversationController.getOurConversationId();
|
const ourId = ConversationController.getOurConversationId();
|
||||||
|
const formattedContact = this.findAndFormatContact(sourceId);
|
||||||
|
|
||||||
const basicProps = {
|
const basicProps = {
|
||||||
...this.findAndFormatContact(sourceId),
|
...formattedContact,
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
timespan,
|
timespan,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -434,6 +447,12 @@
|
||||||
type: 'fromMe',
|
type: 'fromMe',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!sourceId) {
|
||||||
|
return {
|
||||||
|
...basicProps,
|
||||||
|
type: 'fromMember',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return basicProps;
|
return basicProps;
|
||||||
},
|
},
|
||||||
|
@ -473,10 +492,6 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholderContact = {
|
|
||||||
title: i18n('unknownContact'),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (groupUpdate.joined) {
|
if (groupUpdate.joined) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: 'add',
|
type: 'add',
|
||||||
|
@ -484,8 +499,7 @@
|
||||||
Array.isArray(groupUpdate.joined)
|
Array.isArray(groupUpdate.joined)
|
||||||
? groupUpdate.joined
|
? groupUpdate.joined
|
||||||
: [groupUpdate.joined],
|
: [groupUpdate.joined],
|
||||||
identifier =>
|
identifier => this.findAndFormatContact(identifier)
|
||||||
this.findAndFormatContact(identifier) || placeholderContact
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -502,8 +516,7 @@
|
||||||
Array.isArray(groupUpdate.left)
|
Array.isArray(groupUpdate.left)
|
||||||
? groupUpdate.left
|
? groupUpdate.left
|
||||||
: [groupUpdate.left],
|
: [groupUpdate.left],
|
||||||
identifier =>
|
identifier => this.findAndFormatContact(identifier)
|
||||||
this.findAndFormatContact(identifier) || placeholderContact
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -600,15 +613,6 @@
|
||||||
const reactions = (this.get('reactions') || []).map(re => {
|
const reactions = (this.get('reactions') || []).map(re => {
|
||||||
const c = this.findAndFormatContact(re.fromId);
|
const c = this.findAndFormatContact(re.fromId);
|
||||||
|
|
||||||
if (!c) {
|
|
||||||
return {
|
|
||||||
emoji: re.emoji,
|
|
||||||
from: {
|
|
||||||
id: re.fromId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
emoji: re.emoji,
|
emoji: re.emoji,
|
||||||
timestamp: re.timestamp,
|
timestamp: re.timestamp,
|
||||||
|
@ -661,17 +665,29 @@
|
||||||
|
|
||||||
// Dependencies of prop-generation functions
|
// Dependencies of prop-generation functions
|
||||||
findAndFormatContact(identifier) {
|
findAndFormatContact(identifier) {
|
||||||
|
if (!identifier) {
|
||||||
|
return PLACEHOLDER_CONTACT;
|
||||||
|
}
|
||||||
|
|
||||||
const contactModel = this.findContact(identifier);
|
const contactModel = this.findContact(identifier);
|
||||||
if (contactModel) {
|
if (contactModel) {
|
||||||
return contactModel.format();
|
return contactModel.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { format } = PhoneNumber;
|
const { format, isValidNumber } = PhoneNumber;
|
||||||
const regionCode = storage.get('regionCode');
|
const regionCode = storage.get('regionCode');
|
||||||
|
|
||||||
|
if (!isValidNumber(identifier, { regionCode })) {
|
||||||
|
return PLACEHOLDER_CONTACT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneNumber = format(identifier, {
|
||||||
|
ourRegionCode: regionCode,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
phoneNumber: format(identifier, {
|
title: phoneNumber,
|
||||||
ourRegionCode: regionCode,
|
phoneNumber,
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
findContact(identifier) {
|
findContact(identifier) {
|
||||||
|
@ -910,6 +926,29 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isGroupV2Change()) {
|
||||||
|
const { protobuf } = window.textsecure;
|
||||||
|
const change = this.get('groupV2Change');
|
||||||
|
|
||||||
|
const lines = window.Signal.GroupChange.renderChange(change, {
|
||||||
|
AccessControlEnum: protobuf.AccessControl.AccessRequired,
|
||||||
|
i18n: window.i18n,
|
||||||
|
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||||
|
renderContact: conversationId => {
|
||||||
|
const conversation = window.ConversationController.get(
|
||||||
|
conversationId
|
||||||
|
);
|
||||||
|
return conversation
|
||||||
|
? conversation.getTitle()
|
||||||
|
: window.i18n('unknownUser');
|
||||||
|
},
|
||||||
|
renderString: (key, i18n, placeholders) => i18n(key, placeholders),
|
||||||
|
RoleEnum: protobuf.Member.Role,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { text: lines.join(' ') };
|
||||||
|
}
|
||||||
|
|
||||||
const attachments = this.get('attachments') || [];
|
const attachments = this.get('attachments') || [];
|
||||||
|
|
||||||
if (this.isTapToView()) {
|
if (this.isTapToView()) {
|
||||||
|
@ -1315,6 +1354,7 @@
|
||||||
// Rendered sync messages
|
// Rendered sync messages
|
||||||
const isCallHistory = this.isCallHistory();
|
const isCallHistory = this.isCallHistory();
|
||||||
const isGroupUpdate = this.isGroupUpdate();
|
const isGroupUpdate = this.isGroupUpdate();
|
||||||
|
const isGroupV2Change = this.isGroupV2Change();
|
||||||
const isEndSession = this.isEndSession();
|
const isEndSession = this.isEndSession();
|
||||||
const isExpirationTimerUpdate = this.isExpirationTimerUpdate();
|
const isExpirationTimerUpdate = this.isExpirationTimerUpdate();
|
||||||
const isVerifiedChange = this.isVerifiedChange();
|
const isVerifiedChange = this.isVerifiedChange();
|
||||||
|
@ -1342,6 +1382,7 @@
|
||||||
// Rendered sync messages
|
// Rendered sync messages
|
||||||
isCallHistory ||
|
isCallHistory ||
|
||||||
isGroupUpdate ||
|
isGroupUpdate ||
|
||||||
|
isGroupV2Change ||
|
||||||
isEndSession ||
|
isEndSession ||
|
||||||
isExpirationTimerUpdate ||
|
isExpirationTimerUpdate ||
|
||||||
isVerifiedChange ||
|
isVerifiedChange ||
|
||||||
|
@ -1634,6 +1675,8 @@
|
||||||
// Because this is a partial group send, we manually construct the request like
|
// Because this is a partial group send, we manually construct the request like
|
||||||
// sendMessageToGroup does.
|
// sendMessageToGroup does.
|
||||||
|
|
||||||
|
const groupV2 = conversation.getGroupV2Info();
|
||||||
|
|
||||||
promise = textsecure.messaging.sendMessage(
|
promise = textsecure.messaging.sendMessage(
|
||||||
{
|
{
|
||||||
recipients,
|
recipients,
|
||||||
|
@ -1645,10 +1688,13 @@
|
||||||
sticker: stickerWithData,
|
sticker: stickerWithData,
|
||||||
expireTimer: this.get('expireTimer'),
|
expireTimer: this.get('expireTimer'),
|
||||||
profileKey,
|
profileKey,
|
||||||
group: {
|
groupV2,
|
||||||
id: this.getConversation().get('groupId'),
|
group: groupV2
|
||||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
? null
|
||||||
},
|
: {
|
||||||
|
id: this.getConversation().get('groupId'),
|
||||||
|
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
@ -2392,19 +2438,79 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We drop incoming messages for groups we already know about, which we're not a
|
const existingRevision = conversation.get('revision');
|
||||||
// part of, except for group updates.
|
const isGroupV2 = Boolean(initialMessage.groupV2);
|
||||||
const ourUuid = textsecure.storage.user.getUuid();
|
const isV2GroupUpdate =
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
initialMessage.groupV2 &&
|
||||||
const isGroupUpdate =
|
(!existingRevision ||
|
||||||
|
initialMessage.groupV2.revision > existingRevision);
|
||||||
|
|
||||||
|
// GroupV2
|
||||||
|
if (isGroupV2) {
|
||||||
|
conversation.maybeRepairGroupV2(
|
||||||
|
_.pick(initialMessage.groupV2, [
|
||||||
|
'masterKey',
|
||||||
|
'secretParams',
|
||||||
|
'publicParams',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isV2GroupUpdate) {
|
||||||
|
const { revision, groupChange } = initialMessage.groupV2;
|
||||||
|
try {
|
||||||
|
await window.Signal.Groups.maybeUpdateGroup({
|
||||||
|
conversation,
|
||||||
|
groupChangeBase64: groupChange,
|
||||||
|
newRevision: revision,
|
||||||
|
timestamp: message.get('received_at'),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = error && error.stack ? error.stack : error;
|
||||||
|
window.log.error(
|
||||||
|
`handleDataMessage: Failed to process group update for ${conversation.idForLogging()} as part of message ${message.idForLogging()}: ${errorText}`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ourConversationId = ConversationController.getOurConversationId();
|
||||||
|
const senderId = ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
});
|
||||||
|
const isV1GroupUpdate =
|
||||||
initialMessage.group &&
|
initialMessage.group &&
|
||||||
initialMessage.group.type !==
|
initialMessage.group.type !==
|
||||||
textsecure.protobuf.GroupContext.Type.DELIVER;
|
textsecure.protobuf.GroupContext.Type.DELIVER;
|
||||||
|
|
||||||
|
// Drop an incoming GroupV2 message if we or the sender are not part of the group
|
||||||
|
// after applying the message's associated group chnages.
|
||||||
if (
|
if (
|
||||||
type === 'incoming' &&
|
type === 'incoming' &&
|
||||||
!conversation.isPrivate() &&
|
!conversation.isPrivate() &&
|
||||||
!conversation.hasMember(ourNumber || ourUuid) &&
|
isGroupV2 &&
|
||||||
!isGroupUpdate
|
(conversation.get('left') ||
|
||||||
|
!conversation.hasMember(ourConversationId) ||
|
||||||
|
!conversation.hasMember(senderId))
|
||||||
|
) {
|
||||||
|
window.log.warn(
|
||||||
|
`Received message destined for group ${conversation.idForLogging()}, which we or the sender are not a part of. Dropping.`
|
||||||
|
);
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We drop incoming messages for v1 groups we already know about, which we're not
|
||||||
|
// a part of, except for group updates. Because group v1 updates haven't been
|
||||||
|
// applied by this point.
|
||||||
|
if (
|
||||||
|
type === 'incoming' &&
|
||||||
|
!conversation.isPrivate() &&
|
||||||
|
!isGroupV2 &&
|
||||||
|
!isV1GroupUpdate &&
|
||||||
|
(conversation.get('left') ||
|
||||||
|
!conversation.hasMember(ourConversationId))
|
||||||
) {
|
) {
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
||||||
|
@ -2488,7 +2594,9 @@
|
||||||
let attributes = {
|
let attributes = {
|
||||||
...conversation.attributes,
|
...conversation.attributes,
|
||||||
};
|
};
|
||||||
if (dataMessage.group) {
|
|
||||||
|
// GroupV1
|
||||||
|
if (!isGroupV2 && dataMessage.group) {
|
||||||
const pendingGroupUpdate = [];
|
const pendingGroupUpdate = [];
|
||||||
const memberConversations = await Promise.all(
|
const memberConversations = await Promise.all(
|
||||||
dataMessage.group.membersE164.map(e164 =>
|
dataMessage.group.membersE164.map(e164 =>
|
||||||
|
@ -2597,10 +2705,6 @@
|
||||||
conversation.set({ addedBy: message.getContactId() });
|
conversation.set({ addedBy: message.getContactId() });
|
||||||
}
|
}
|
||||||
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
||||||
const senderId = ConversationController.ensureContactIds({
|
|
||||||
e164: source,
|
|
||||||
uuid: sourceUuid,
|
|
||||||
});
|
|
||||||
const sender = ConversationController.get(senderId);
|
const sender = ConversationController.get(senderId);
|
||||||
const inGroup = Boolean(
|
const inGroup = Boolean(
|
||||||
sender &&
|
sender &&
|
||||||
|
@ -2638,6 +2742,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop empty messages after. This needs to happen after the initial
|
||||||
|
// message.set call and after GroupV1 processing to make sure all possible
|
||||||
|
// properties are set before we determine that a message is empty.
|
||||||
|
if (message.isEmpty()) {
|
||||||
|
window.log.info(
|
||||||
|
`handleDataMessage: Dropping empty message ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'outgoing') {
|
if (type === 'outgoing') {
|
||||||
const receipts = Whisper.DeliveryReceipts.forMessage(
|
const receipts = Whisper.DeliveryReceipts.forMessage(
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -2652,61 +2767,66 @@
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.active_at = now;
|
attributes.active_at = now;
|
||||||
conversation.set(attributes);
|
conversation.set(attributes);
|
||||||
|
|
||||||
if (message.isExpirationTimerUpdate()) {
|
if (dataMessage.expireTimer) {
|
||||||
message.set({
|
|
||||||
expirationTimerUpdate: {
|
|
||||||
source,
|
|
||||||
sourceUuid,
|
|
||||||
expireTimer: dataMessage.expireTimer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
conversation.set({ expireTimer: dataMessage.expireTimer });
|
|
||||||
} else if (dataMessage.expireTimer) {
|
|
||||||
message.set({ expireTimer: dataMessage.expireTimer });
|
message.set({ expireTimer: dataMessage.expireTimer });
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Remove once the above uses
|
if (!isGroupV2) {
|
||||||
// `Conversation::updateExpirationTimer`:
|
if (message.isExpirationTimerUpdate()) {
|
||||||
const { expireTimer } = dataMessage;
|
message.set({
|
||||||
const shouldLogExpireTimerChange =
|
expirationTimerUpdate: {
|
||||||
message.isExpirationTimerUpdate() || expireTimer;
|
source,
|
||||||
if (shouldLogExpireTimerChange) {
|
sourceUuid,
|
||||||
window.log.info("Update conversation 'expireTimer'", {
|
expireTimer: dataMessage.expireTimer,
|
||||||
id: conversation.idForLogging(),
|
},
|
||||||
expireTimer,
|
});
|
||||||
source: 'handleDataMessage',
|
conversation.set({ expireTimer: dataMessage.expireTimer });
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.isEndSession()) {
|
// NOTE: Remove once the above calls this.model.updateExpirationTimer()
|
||||||
if (dataMessage.expireTimer) {
|
const { expireTimer } = dataMessage;
|
||||||
if (
|
const shouldLogExpireTimerChange =
|
||||||
dataMessage.expireTimer !== conversation.get('expireTimer')
|
message.isExpirationTimerUpdate() || expireTimer;
|
||||||
|
if (shouldLogExpireTimerChange) {
|
||||||
|
window.log.info("Update conversation 'expireTimer'", {
|
||||||
|
id: conversation.idForLogging(),
|
||||||
|
expireTimer,
|
||||||
|
source: 'handleDataMessage',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.isEndSession()) {
|
||||||
|
if (dataMessage.expireTimer) {
|
||||||
|
if (
|
||||||
|
dataMessage.expireTimer !== conversation.get('expireTimer')
|
||||||
|
) {
|
||||||
|
conversation.updateExpirationTimer(
|
||||||
|
dataMessage.expireTimer,
|
||||||
|
source,
|
||||||
|
message.get('received_at'),
|
||||||
|
{
|
||||||
|
fromGroupUpdate: message.isGroupUpdate(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
conversation.get('expireTimer') &&
|
||||||
|
// We only turn off timers if it's not a group update
|
||||||
|
!message.isGroupUpdate()
|
||||||
) {
|
) {
|
||||||
conversation.updateExpirationTimer(
|
conversation.updateExpirationTimer(
|
||||||
dataMessage.expireTimer,
|
null,
|
||||||
source,
|
source,
|
||||||
message.get('received_at'),
|
message.get('received_at')
|
||||||
{
|
|
||||||
fromGroupUpdate: message.isGroupUpdate(),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
|
||||||
conversation.get('expireTimer') &&
|
|
||||||
// We only turn off timers if it's not a group update
|
|
||||||
!message.isGroupUpdate()
|
|
||||||
) {
|
|
||||||
conversation.updateExpirationTimer(
|
|
||||||
null,
|
|
||||||
source,
|
|
||||||
message.get('received_at')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'incoming') {
|
if (type === 'incoming') {
|
||||||
const readSync = Whisper.ReadSyncs.forMessage(message);
|
const readSync = Whisper.ReadSyncs.forMessage(message);
|
||||||
if (readSync) {
|
if (readSync) {
|
||||||
|
@ -2804,17 +2924,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop empty messages. This needs to happen after the initial
|
|
||||||
// message.set call to make sure all possible properties are set
|
|
||||||
// before we determine that a message is empty.
|
|
||||||
if (message.isEmpty()) {
|
|
||||||
window.log.info(
|
|
||||||
`Dropping empty datamessage ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
|
||||||
);
|
|
||||||
confirm();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversationTimestamp = conversation.get('timestamp');
|
const conversationTimestamp = conversation.get('timestamp');
|
||||||
if (
|
if (
|
||||||
!conversationTimestamp ||
|
!conversationTimestamp ||
|
||||||
|
|
|
@ -10,6 +10,7 @@ const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
||||||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||||
const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{9}([0-9A-F]{3})/gi;
|
const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{9}([0-9A-F]{3})/gi;
|
||||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||||
|
const GROUP_V2_ID_PATTERN = /(groupv2\()([^=)]+)(=?=?\))/g;
|
||||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||||
|
|
||||||
// _redactPath :: Path -> String -> String
|
// _redactPath :: Path -> String -> String
|
||||||
|
@ -80,11 +81,21 @@ exports.redactGroupIds = text => {
|
||||||
throw new TypeError("'text' must be a string");
|
throw new TypeError("'text' must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.replace(
|
return text
|
||||||
GROUP_ID_PATTERN,
|
.replace(
|
||||||
(match, before, id, after) =>
|
GROUP_ID_PATTERN,
|
||||||
`${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice(-3)}${after}`
|
(match, before, id, after) =>
|
||||||
);
|
`${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice(
|
||||||
|
-3
|
||||||
|
)}${after}`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
GROUP_V2_ID_PATTERN,
|
||||||
|
(match, before, id, after) =>
|
||||||
|
`${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice(
|
||||||
|
-3
|
||||||
|
)}${after}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// redactSensitivePaths :: String -> String
|
// redactSensitivePaths :: String -> String
|
||||||
|
|
|
@ -9,6 +9,8 @@ const {
|
||||||
const Data = require('../../ts/sql/Client').default;
|
const Data = require('../../ts/sql/Client').default;
|
||||||
const Emojis = require('./emojis');
|
const Emojis = require('./emojis');
|
||||||
const EmojiLib = require('../../ts/components/emoji/lib');
|
const EmojiLib = require('../../ts/components/emoji/lib');
|
||||||
|
const Groups = require('../../ts/groups');
|
||||||
|
const GroupChange = require('../../ts/groupChange');
|
||||||
const IndexedDB = require('./indexeddb');
|
const IndexedDB = require('./indexeddb');
|
||||||
const Notifications = require('../../ts/notifications');
|
const Notifications = require('../../ts/notifications');
|
||||||
const OS = require('../../ts/OS');
|
const OS = require('../../ts/OS');
|
||||||
|
@ -108,6 +110,9 @@ const { IdleDetector } = require('./idle_detector');
|
||||||
const MessageDataMigrator = require('./messages_data_migrator');
|
const MessageDataMigrator = require('./messages_data_migrator');
|
||||||
|
|
||||||
// Processes / Services
|
// Processes / Services
|
||||||
|
const {
|
||||||
|
initializeGroupCredentialFetcher,
|
||||||
|
} = require('../../ts/services/groupCredentialFetcher');
|
||||||
const {
|
const {
|
||||||
initializeNetworkObserver,
|
initializeNetworkObserver,
|
||||||
} = require('../../ts/services/networkObserver');
|
} = require('../../ts/services/networkObserver');
|
||||||
|
@ -333,6 +338,7 @@ exports.setup = (options = {}) => {
|
||||||
calling,
|
calling,
|
||||||
eraseAllStorageServiceState,
|
eraseAllStorageServiceState,
|
||||||
handleUnknownRecords,
|
handleUnknownRecords,
|
||||||
|
initializeGroupCredentialFetcher,
|
||||||
initializeNetworkObserver,
|
initializeNetworkObserver,
|
||||||
initializeUpdateListener,
|
initializeUpdateListener,
|
||||||
notify,
|
notify,
|
||||||
|
@ -378,6 +384,8 @@ exports.setup = (options = {}) => {
|
||||||
Data,
|
Data,
|
||||||
Emojis,
|
Emojis,
|
||||||
EmojiLib,
|
EmojiLib,
|
||||||
|
Groups,
|
||||||
|
GroupChange,
|
||||||
IndexedDB,
|
IndexedDB,
|
||||||
LinkPreviews,
|
LinkPreviews,
|
||||||
Metadata,
|
Metadata,
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
/* global crypto, window */
|
/* global window */
|
||||||
|
|
||||||
const { isFunction, isNumber } = require('lodash');
|
const { isFunction, isNumber } = require('lodash');
|
||||||
const {
|
const {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
base64ToArrayBuffer,
|
base64ToArrayBuffer,
|
||||||
|
computeHash,
|
||||||
} = require('../../../ts/Crypto');
|
} = require('../../../ts/Crypto');
|
||||||
|
|
||||||
async function computeHash(arraybuffer) {
|
|
||||||
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer);
|
|
||||||
return arrayBufferToBase64(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAvatarUpdater({ field }) {
|
function buildAvatarUpdater({ field }) {
|
||||||
return async (conversation, data, options = {}) => {
|
return async (conversation, data, options = {}) => {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
if (conversation.isPrivate()) {
|
if (conversation.isPrivate()) {
|
||||||
ids = [conversation.id];
|
ids = [conversation.id];
|
||||||
} else {
|
} else {
|
||||||
ids = conversation.get('members');
|
ids = conversation.getMemberIds();
|
||||||
}
|
}
|
||||||
const receipts = this.filter(
|
const receipts = this.filter(
|
||||||
receipt =>
|
receipt =>
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
className: 'contact-wrapper',
|
className: 'contact-wrapper',
|
||||||
Component: window.Signal.Components.ContactListItem,
|
Component: window.Signal.Components.ContactListItem,
|
||||||
props: {
|
props: {
|
||||||
...this.model.cachedProps,
|
...this.model.format(),
|
||||||
onClick: this.showIdentity.bind(this),
|
onClick: this.showIdentity.bind(this),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -215,6 +215,9 @@
|
||||||
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
||||||
template: i18n('maximumAttachments'),
|
template: i18n('maximumAttachments'),
|
||||||
});
|
});
|
||||||
|
Whisper.TimerConflictToast = Whisper.ToastView.extend({
|
||||||
|
template: i18n('GroupV2--timerConflict'),
|
||||||
|
});
|
||||||
|
|
||||||
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
||||||
templateName: 'conversation-loading-screen',
|
templateName: 'conversation-loading-screen',
|
||||||
|
@ -311,6 +314,13 @@
|
||||||
this.model.updateSharedGroups.bind(this.model),
|
this.model.updateSharedGroups.bind(this.model),
|
||||||
FIVE_MINUTES
|
FIVE_MINUTES
|
||||||
);
|
);
|
||||||
|
this.model.throttledFetchLatestGroupV2Data =
|
||||||
|
this.model.throttledFetchLatestGroupV2Data ||
|
||||||
|
_.throttle(
|
||||||
|
this.model.fetchLatestGroupV2Data.bind(this.model),
|
||||||
|
FIVE_MINUTES
|
||||||
|
);
|
||||||
|
|
||||||
this.debouncedMaybeGrabLinkPreview = _.debounce(
|
this.debouncedMaybeGrabLinkPreview = _.debounce(
|
||||||
this.maybeGrabLinkPreview.bind(this),
|
this.maybeGrabLinkPreview.bind(this),
|
||||||
200
|
200
|
||||||
|
@ -385,8 +395,13 @@
|
||||||
|
|
||||||
leftGroup: this.model.get('left'),
|
leftGroup: this.model.get('left'),
|
||||||
|
|
||||||
expirationSettingName,
|
disableTimerChanges:
|
||||||
|
this.model.get('left') ||
|
||||||
|
!this.model.getAccepted() ||
|
||||||
|
!this.model.canChangeTimer(),
|
||||||
showBackButton: Boolean(this.panels && this.panels.length),
|
showBackButton: Boolean(this.panels && this.panels.length),
|
||||||
|
|
||||||
|
expirationSettingName,
|
||||||
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
|
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
|
||||||
name: item.getName(),
|
name: item.getName(),
|
||||||
value: item.get('seconds'),
|
value: item.get('seconds'),
|
||||||
|
@ -1826,6 +1841,8 @@
|
||||||
this.setQuoteMessage(quotedMessageId);
|
this.setQuoteMessage(quotedMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.model.throttledFetchLatestGroupV2Data();
|
||||||
|
|
||||||
const statusPromise = this.model.throttledGetProfiles();
|
const statusPromise = this.model.throttledGetProfiles();
|
||||||
// eslint-disable-next-line more/no-then
|
// eslint-disable-next-line more/no-then
|
||||||
this.statusFetch = statusPromise.then(() =>
|
this.statusFetch = statusPromise.then(() =>
|
||||||
|
@ -2044,7 +2061,18 @@
|
||||||
async showMembers(e, providedMembers, options = {}) {
|
async showMembers(e, providedMembers, options = {}) {
|
||||||
_.defaults(options, { needVerify: false });
|
_.defaults(options, { needVerify: false });
|
||||||
|
|
||||||
const model = providedMembers || this.model.contactCollection;
|
let model = providedMembers || this.model.contactCollection;
|
||||||
|
|
||||||
|
if (!providedMembers && this.model.get('groupVersion') === 2) {
|
||||||
|
model = new Whisper.GroupConversationCollection(
|
||||||
|
this.model.get('membersV2').map(({ conversationId, role }) => ({
|
||||||
|
conversation: ConversationController.get(conversationId),
|
||||||
|
isAdmin:
|
||||||
|
role === window.textsecure.protobuf.Member.Role.ADMINISTRATOR,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const view = new Whisper.GroupMemberList({
|
const view = new Whisper.GroupMemberList({
|
||||||
model,
|
model,
|
||||||
// we pass this in to allow nested panels
|
// we pass this in to allow nested panels
|
||||||
|
@ -2496,11 +2524,17 @@
|
||||||
this.model.endSession();
|
this.model.endSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
setDisappearingMessages(seconds) {
|
async setDisappearingMessages(seconds) {
|
||||||
if (seconds > 0) {
|
try {
|
||||||
this.model.updateExpirationTimer(seconds);
|
if (seconds > 0) {
|
||||||
} else {
|
await this.model.updateExpirationTimer(seconds);
|
||||||
this.model.updateExpirationTimer(null);
|
} else {
|
||||||
|
await this.model.updateExpirationTimer(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 409) {
|
||||||
|
this.showToast(Whisper.TimerConflictToast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
}
|
}
|
||||||
const protos = result.build('signalservice');
|
const protos = result.build('signalservice');
|
||||||
if (!protos) {
|
if (!protos) {
|
||||||
const text = `Error loading protos from ${filename} (root: ${window.PROTO_ROOT})`;
|
const text = `Error loading protos from ${filename} - no exported types! (root: ${window.PROTO_ROOT})`;
|
||||||
window.log.error(text);
|
window.log.error(text);
|
||||||
throw new Error(text);
|
throw new Error(text);
|
||||||
}
|
}
|
||||||
|
@ -41,4 +41,7 @@
|
||||||
|
|
||||||
// Metadata-specific protos
|
// Metadata-specific protos
|
||||||
loadProtoBufs('UnidentifiedDelivery.proto');
|
loadProtoBufs('UnidentifiedDelivery.proto');
|
||||||
|
|
||||||
|
// Groups
|
||||||
|
loadProtoBufs('Groups.proto');
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -19,6 +19,8 @@ try {
|
||||||
window.PROTO_ROOT = 'protos';
|
window.PROTO_ROOT = 'protos';
|
||||||
const config = require('url').parse(window.location.toString(), true).query;
|
const config = require('url').parse(window.location.toString(), true).query;
|
||||||
|
|
||||||
|
window.GV2 = false;
|
||||||
|
|
||||||
let title = config.name;
|
let title = config.name;
|
||||||
if (config.environment !== 'production') {
|
if (config.environment !== 'production') {
|
||||||
title += ` - ${config.environment}`;
|
title += ` - ${config.environment}`;
|
||||||
|
@ -375,12 +377,14 @@ try {
|
||||||
paths.forEach(path => {
|
paths.forEach(path => {
|
||||||
const val = _.get(obj, path);
|
const val = _.get(obj, path);
|
||||||
if (val) {
|
if (val) {
|
||||||
if (!window.isValidGuid(val)) {
|
if (!val || !window.isValidGuid(val)) {
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
`Normalizing invalid uuid: ${val} at path ${path} in context "${context}"`
|
`Normalizing invalid uuid: ${val} at path ${path} in context "${context}"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_.set(obj, path, val.toLowerCase());
|
if (val && val.toLowerCase) {
|
||||||
|
_.set(obj, path, val.toLowerCase());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
152
protos/Groups.proto
Normal file
152
protos/Groups.proto
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.protos.groups";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
message AvatarUploadAttributes {
|
||||||
|
string key = 1;
|
||||||
|
string credential = 2;
|
||||||
|
string acl = 3;
|
||||||
|
string algorithm = 4;
|
||||||
|
string date = 5;
|
||||||
|
string policy = 6;
|
||||||
|
string signature = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Member {
|
||||||
|
enum Role {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
DEFAULT = 1; // Normal member
|
||||||
|
ADMINISTRATOR = 2; // Group admin
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes userId = 1; // The UuidCiphertext
|
||||||
|
Role role = 2;
|
||||||
|
bytes profileKey = 3; // The ProfileKeyCiphertext
|
||||||
|
bytes presentation = 4; // ProfileKeyCredentialPresentation
|
||||||
|
uint32 joinedAtVersion = 5; // The Group.version this member joined at
|
||||||
|
}
|
||||||
|
|
||||||
|
message PendingMember {
|
||||||
|
Member member = 1; // The “invited” member
|
||||||
|
bytes addedByUserId = 2; // The UID who invited this member
|
||||||
|
uint64 timestamp = 3; // The time the invitation occurred
|
||||||
|
}
|
||||||
|
|
||||||
|
message AccessControl {
|
||||||
|
enum AccessRequired {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
MEMBER = 2; // Any group member can make the modification
|
||||||
|
ADMINISTRATOR = 3; // Only administrators can make the modification
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessRequired attributes = 1; // Who can modify the group title, avatar, disappearing messages timer
|
||||||
|
AccessRequired members = 2; // Who can add people to the group
|
||||||
|
}
|
||||||
|
|
||||||
|
message Group {
|
||||||
|
bytes publicKey = 1; // GroupPublicParams
|
||||||
|
bytes title = 2; // Encrypted title
|
||||||
|
string avatar = 3; // Pointer to encrypted avatar (‘key’ from AvatarUploadAttributes)
|
||||||
|
bytes disappearingMessagesTimer = 4; // Encrypted timer
|
||||||
|
AccessControl accessControl = 5;
|
||||||
|
uint32 version = 6; // Current group version number
|
||||||
|
repeated Member members = 7;
|
||||||
|
repeated PendingMember pendingMembers = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupChange {
|
||||||
|
|
||||||
|
message Actions {
|
||||||
|
|
||||||
|
message AddMemberAction {
|
||||||
|
Member added = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteMemberAction {
|
||||||
|
bytes deletedUserId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyMemberRoleAction {
|
||||||
|
bytes userId = 1;
|
||||||
|
Member.Role role = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyMemberProfileKeyAction {
|
||||||
|
bytes presentation = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddPendingMemberAction {
|
||||||
|
PendingMember added = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePendingMemberAction {
|
||||||
|
bytes deletedUserId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PromotePendingMemberAction {
|
||||||
|
bytes presentation = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyTitleAction {
|
||||||
|
bytes title = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyAvatarAction {
|
||||||
|
string avatar = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyDisappearingMessagesTimerAction {
|
||||||
|
bytes timer = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyAttributesAccessControlAction {
|
||||||
|
AccessControl.AccessRequired attributesAccess = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyAvatarAccessControlAction {
|
||||||
|
AccessControl.AccessRequired avatarAccess = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ModifyMembersAccessControlAction {
|
||||||
|
AccessControl.AccessRequired membersAccess = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes sourceUuid = 1; // Who made the change
|
||||||
|
uint32 version = 2; // The change version number
|
||||||
|
repeated AddMemberAction addMembers = 3; // Members added
|
||||||
|
repeated DeleteMemberAction deleteMembers = 4; // Members deleted
|
||||||
|
repeated ModifyMemberRoleAction modifyMemberRoles = 5; // Modified member roles
|
||||||
|
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6; // Modified member profile keys
|
||||||
|
repeated AddPendingMemberAction addPendingMembers = 7; // Pending members added
|
||||||
|
repeated DeletePendingMemberAction deletePendingMembers = 8; // Pending members deleted
|
||||||
|
repeated PromotePendingMemberAction promotePendingMembers = 9; // Pending invitations accepted
|
||||||
|
ModifyTitleAction modifyTitle = 10; // Changed title
|
||||||
|
ModifyAvatarAction modifyAvatar = 11; // Changed avatar
|
||||||
|
ModifyDisappearingMessagesTimerAction modifyDisappearingMessagesTimer = 12; // Changed timer
|
||||||
|
ModifyAttributesAccessControlAction modifyAttributesAccess = 13; // Changed attributes access control
|
||||||
|
ModifyMembersAccessControlAction modifyMemberAccess = 14; // Changed membership access control
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes actions = 1; // The serialized actions
|
||||||
|
bytes serverSignature = 2; // Server’s signature over serialized actions
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupChanges {
|
||||||
|
message GroupChangeState {
|
||||||
|
GroupChange groupChange = 1;
|
||||||
|
Group groupState = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeated GroupChangeState groupChanges = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupAttributeBlob {
|
||||||
|
oneof content {
|
||||||
|
string title = 1;
|
||||||
|
bytes avatar = 2;
|
||||||
|
uint32 disappearingMessagesDuration = 3;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2581,7 +2581,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
color: $color-gray-60;
|
color: $color-gray-60;
|
||||||
}
|
}
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
color: $color-gray-25;
|
color: $color-gray-05;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2603,7 +2603,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
@include color-svg('../images/icons/v2/timer-24.svg', $color-gray-60);
|
@include color-svg('../images/icons/v2/timer-24.svg', $color-gray-60);
|
||||||
}
|
}
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
@include color-svg('../images/icons/v2/timer-24.svg', $color-gray-25);
|
@include color-svg('../images/icons/v2/timer-24.svg', $color-gray-05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2617,7 +2617,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
'../images/icons/v2/timer-disabled-24.svg',
|
'../images/icons/v2/timer-disabled-24.svg',
|
||||||
$color-gray-25
|
$color-gray-05
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2689,6 +2689,11 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
|
|
||||||
.module-contact-list-item__text {
|
.module-contact-list-item__text {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-contact-list-item__text__name {
|
.module-contact-list-item__text__name {
|
||||||
|
@ -2722,6 +2727,14 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-contact-list-item__admin {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@include font-body-2-bold;
|
||||||
|
}
|
||||||
|
|
||||||
// Module: In Contacts Icon
|
// Module: In Contacts Icon
|
||||||
|
|
||||||
.module-in-contacts-icon__icon {
|
.module-in-contacts-icon__icon {
|
||||||
|
@ -9001,6 +9014,45 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: GroupV2 Change
|
||||||
|
|
||||||
|
.module-group-v2-change {
|
||||||
|
@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-group-v2-change--icon {
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/group-outline-20.svg',
|
||||||
|
$color-gray-60
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/group-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 {
|
||||||
|
|
|
@ -161,7 +161,7 @@ describe('Message', () => {
|
||||||
left: 'You',
|
left: 'You',
|
||||||
},
|
},
|
||||||
}).getNotificationData(),
|
}).getNotificationData(),
|
||||||
{ text: 'You left the group.' }
|
{ text: 'You are no longer a member of the group.' }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,18 @@ describe('Privacy', () => {
|
||||||
'and group([REDACTED]hij)';
|
'and group([REDACTED]hij)';
|
||||||
assert.equal(actual, expected);
|
assert.equal(actual, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should remove newlines from redacted group V2 IDs', () => {
|
||||||
|
const text =
|
||||||
|
'This is a log line with three group IDs: groupv2(abcd32341a==)\n' +
|
||||||
|
'and groupv2(abcd32341ad=) and and groupv2(abcd32341ade)';
|
||||||
|
|
||||||
|
const actual = Privacy.redactGroupIds(text);
|
||||||
|
const expected =
|
||||||
|
'This is a log line with three group IDs: groupv2([REDACTED]41a==)\n' +
|
||||||
|
'and groupv2([REDACTED]1ad=) and and groupv2([REDACTED]ade)';
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('redactAll', () => {
|
describe('redactAll', () => {
|
||||||
|
|
|
@ -217,7 +217,7 @@ export class ConversationController {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
getOurConversationId() {
|
getOurConversationId(): string | undefined {
|
||||||
const e164 = window.textsecure.storage.user.getNumber();
|
const e164 = window.textsecure.storage.user.getNumber();
|
||||||
const uuid = window.textsecure.storage.user.getUuid();
|
const uuid = window.textsecure.storage.user.getUuid();
|
||||||
return this.ensureContactIds({ e164, uuid, highTrust: true });
|
return this.ensureContactIds({ e164, uuid, highTrust: true });
|
||||||
|
@ -238,7 +238,7 @@ export class ConversationController {
|
||||||
e164?: string;
|
e164?: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
highTrust?: boolean;
|
highTrust?: boolean;
|
||||||
}) {
|
}): string | undefined {
|
||||||
// Check for at least one parameter being provided. This is necessary
|
// Check for at least one parameter being provided. This is necessary
|
||||||
// because this path can be called on startup to resolve our own ID before
|
// because this path can be called on startup to resolve our own ID before
|
||||||
// our phone number or UUID are known. The existing behavior in these
|
// our phone number or UUID are known. The existing behavior in these
|
||||||
|
@ -546,7 +546,7 @@ export class ConversationController {
|
||||||
* ensures the existence of a group conversation and returns a string
|
* ensures the existence of a group conversation and returns a string
|
||||||
* representing the local database ID of the group conversation.
|
* representing the local database ID of the group conversation.
|
||||||
*/
|
*/
|
||||||
ensureGroup(groupId: string, additionalInitProps = {}) {
|
ensureGroup(groupId: string, additionalInitProps = {}): string {
|
||||||
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
|
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -62,6 +62,11 @@ export async function deriveStickerPackKey(packKey: ArrayBuffer) {
|
||||||
return concatenateBytes(part1, part2);
|
return concatenateBytes(part1, part2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function computeHash(data: ArrayBuffer): Promise<string> {
|
||||||
|
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, data);
|
||||||
|
return arrayBufferToBase64(hash);
|
||||||
|
}
|
||||||
|
|
||||||
// High-level Operations
|
// High-level Operations
|
||||||
|
|
||||||
export async function encryptDeviceName(
|
export async function encryptDeviceName(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { get, throttle } from 'lodash';
|
import { get, throttle } from 'lodash';
|
||||||
import { WebAPIType } from './textsecure/WebAPI';
|
import { WebAPIType } from './textsecure/WebAPI';
|
||||||
|
|
||||||
type ConfigKeyType = 'desktop.messageRequests';
|
type ConfigKeyType = 'desktop.messageRequests' | 'desktop.gv2' | 'desktop.cds';
|
||||||
type ConfigValueType = {
|
type ConfigValueType = {
|
||||||
name: ConfigKeyType;
|
name: ConfigKeyType;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
@ -66,6 +66,7 @@ export const refreshRemoteConfig = async () => {
|
||||||
// If enablement changes at all, notify listeners
|
// If enablement changes at all, notify listeners
|
||||||
const currentListeners = listeners[name] || [];
|
const currentListeners = listeners[name] || [];
|
||||||
if (previouslyEnabled !== enabled) {
|
if (previouslyEnabled !== enabled) {
|
||||||
|
window.log.info(`Remote Config: Flag ${name} has been enabled`);
|
||||||
currentListeners.forEach(listener => {
|
currentListeners.forEach(listener => {
|
||||||
listener(value);
|
listener(value);
|
||||||
});
|
});
|
||||||
|
|
|
@ -69,6 +69,33 @@ storiesOf('Components/ContactListItem', module)
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
.add('With name and profile, admin', () => {
|
||||||
|
return (
|
||||||
|
<ContactListItem
|
||||||
|
i18n={i18n}
|
||||||
|
isAdmin
|
||||||
|
title="Someone 🔥 Somewhere"
|
||||||
|
name="Someone 🔥 Somewhere"
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
profileName="🔥Flames🔥"
|
||||||
|
isVerified
|
||||||
|
avatarPath={gifUrl}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('With just number, admin', () => {
|
||||||
|
return (
|
||||||
|
<ContactListItem
|
||||||
|
i18n={i18n}
|
||||||
|
isAdmin
|
||||||
|
title="(202) 555-0011"
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
avatarPath={gifUrl}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
.add('With name and profile, no avatar', () => {
|
.add('With name and profile, no avatar', () => {
|
||||||
return (
|
return (
|
||||||
<ContactListItem
|
<ContactListItem
|
||||||
|
|
|
@ -9,16 +9,17 @@ import { LocalizerType } from '../types/Util';
|
||||||
import { ColorType } from '../types/Colors';
|
import { ColorType } from '../types/Colors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
|
||||||
phoneNumber?: string;
|
|
||||||
isMe?: boolean;
|
|
||||||
name?: string;
|
|
||||||
color?: ColorType;
|
|
||||||
isVerified?: boolean;
|
|
||||||
profileName?: string;
|
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
|
color?: ColorType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
isMe?: boolean;
|
||||||
|
isVerified?: boolean;
|
||||||
|
name?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
phoneNumber?: string;
|
||||||
|
profileName?: string;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContactListItem extends React.Component<Props> {
|
export class ContactListItem extends React.Component<Props> {
|
||||||
|
@ -51,13 +52,14 @@ export class ContactListItem extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
|
isAdmin,
|
||||||
|
isMe,
|
||||||
|
isVerified,
|
||||||
name,
|
name,
|
||||||
onClick,
|
onClick,
|
||||||
isMe,
|
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
title,
|
title,
|
||||||
isVerified,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const displayName = isMe ? i18n('you') : title;
|
const displayName = isMe ? i18n('you') : title;
|
||||||
|
@ -76,23 +78,30 @@ export class ContactListItem extends React.Component<Props> {
|
||||||
>
|
>
|
||||||
{this.renderAvatar()}
|
{this.renderAvatar()}
|
||||||
<div className="module-contact-list-item__text">
|
<div className="module-contact-list-item__text">
|
||||||
<div className="module-contact-list-item__text__name">
|
<div className="module-contact-list-item__left">
|
||||||
<Emojify text={displayName} />
|
<div className="module-contact-list-item__text__name">
|
||||||
{shouldShowIcon ? (
|
<Emojify text={displayName} />
|
||||||
<span>
|
{shouldShowIcon ? (
|
||||||
{' '}
|
<span>
|
||||||
<InContactsIcon i18n={i18n} />
|
{' '}
|
||||||
</span>
|
<InContactsIcon i18n={i18n} />
|
||||||
) : null}
|
</span>
|
||||||
</div>
|
) : null}
|
||||||
<div className="module-contact-list-item__text__additional-data">
|
</div>
|
||||||
{showVerified ? (
|
<div className="module-contact-list-item__text__additional-data">
|
||||||
<div className="module-contact-list-item__text__verified-icon" />
|
{showVerified ? (
|
||||||
) : null}
|
<div className="module-contact-list-item__text__verified-icon" />
|
||||||
{showVerified ? ` ${i18n('verified')}` : null}
|
) : null}
|
||||||
{showVerified && showNumber ? ' ∙ ' : null}
|
{showVerified ? ` ${i18n('verified')}` : null}
|
||||||
{showNumber ? phoneNumber : null}
|
{showVerified && showNumber ? ' ∙ ' : null}
|
||||||
|
{showNumber ? phoneNumber : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isAdmin ? (
|
||||||
|
<div className="module-contact-list-item__admin">
|
||||||
|
{i18n('GroupV2--admin')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -228,7 +228,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '2',
|
id: '2',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
leftGroup: true,
|
disableTimerChanges: true,
|
||||||
expirationSettingName: '10 seconds',
|
expirationSettingName: '10 seconds',
|
||||||
timerOptions: [
|
timerOptions: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -35,8 +35,8 @@ export interface PropsDataType {
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
isMe?: boolean;
|
isMe?: boolean;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
leftGroup?: boolean;
|
|
||||||
|
|
||||||
|
disableTimerChanges?: boolean;
|
||||||
expirationSettingName?: string;
|
expirationSettingName?: string;
|
||||||
muteExpirationLabel?: string;
|
muteExpirationLabel?: string;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
|
@ -286,12 +286,12 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
|
|
||||||
public renderMenu(triggerId: string) {
|
public renderMenu(triggerId: string) {
|
||||||
const {
|
const {
|
||||||
|
disableTimerChanges,
|
||||||
i18n,
|
i18n,
|
||||||
isAccepted,
|
isAccepted,
|
||||||
isMe,
|
isMe,
|
||||||
type,
|
type,
|
||||||
isArchived,
|
isArchived,
|
||||||
leftGroup,
|
|
||||||
muteExpirationLabel,
|
muteExpirationLabel,
|
||||||
onDeleteMessages,
|
onDeleteMessages,
|
||||||
onResetSession,
|
onResetSession,
|
||||||
|
@ -329,7 +329,7 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu id={triggerId}>
|
<ContextMenu id={triggerId}>
|
||||||
{!leftGroup && isAccepted ? (
|
{disableTimerChanges ? null : (
|
||||||
<SubMenu title={disappearingTitle}>
|
<SubMenu title={disappearingTitle}>
|
||||||
{(timerOptions || []).map(item => (
|
{(timerOptions || []).map(item => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -342,7 +342,7 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
) : null}
|
)}
|
||||||
<SubMenu title={muteTitle}>
|
<SubMenu title={muteTitle}>
|
||||||
{muteOptions.map(item => (
|
{muteOptions.map(item => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
866
ts/components/conversation/GroupV2Change.stories.tsx
Normal file
866
ts/components/conversation/GroupV2Change.stories.tsx
Normal file
|
@ -0,0 +1,866 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { GroupV2ChangeType } from '../../groups';
|
||||||
|
import { SmartContactRendererType } from '../../groupChange';
|
||||||
|
import { GroupV2Change } from './GroupV2Change';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const OUR_ID = 'OUR_ID';
|
||||||
|
const CONTACT_A = 'CONTACT_A';
|
||||||
|
const CONTACT_B = 'CONTACT_B';
|
||||||
|
const CONTACT_C = 'CONTACT_C';
|
||||||
|
const ADMIN_A = 'ADMIN_A';
|
||||||
|
const INVITEE_A = 'INVITEE_A';
|
||||||
|
|
||||||
|
// tslint:disable-next-line no-unnecessary-class
|
||||||
|
class AccessControlEnum {
|
||||||
|
static UNKNOWN = 0;
|
||||||
|
static ADMINISTRATOR = 1;
|
||||||
|
static ANY = 2;
|
||||||
|
static MEMBER = 3;
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line no-unnecessary-class
|
||||||
|
class RoleEnum {
|
||||||
|
static UNKNOWN = 0;
|
||||||
|
static ADMINISTRATOR = 1;
|
||||||
|
static DEFAULT = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContact: SmartContactRendererType = (conversationId: string) => (
|
||||||
|
<React.Fragment key={conversationId}>
|
||||||
|
{`Conversation(${conversationId})`}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderChange = (change: GroupV2ChangeType) => (
|
||||||
|
<GroupV2Change
|
||||||
|
AccessControlEnum={AccessControlEnum}
|
||||||
|
change={change}
|
||||||
|
i18n={i18n}
|
||||||
|
ourConversationId={OUR_ID}
|
||||||
|
renderContact={renderContact}
|
||||||
|
RoleEnum={RoleEnum}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
|
.add('Multiple', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
newTitle: 'Saturday Running',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
newPrivilege: RoleEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Title', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
newTitle: 'Saturday Running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
newTitle: 'Saturday Running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
newTitle: 'Saturday Running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Avatar', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'avatar',
|
||||||
|
removed: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Access (Attributes)', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege: AccessControlEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege: AccessControlEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege: AccessControlEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege: AccessControlEnum.MEMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege: AccessControlEnum.MEMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege: AccessControlEnum.MEMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Access (Members)', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege: AccessControlEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege: AccessControlEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege: AccessControlEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege: AccessControlEnum.MEMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege: AccessControlEnum.MEMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege: AccessControlEnum.MEMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Member Add', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_B,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Member Add - from invite', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_B,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
inviter: CONTACT_B,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Member Remove', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_B,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-remove',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
.add('Member Privilege', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
newPrivilege: RoleEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
newPrivilege: RoleEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
newPrivilege: RoleEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
newPrivilege: RoleEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
newPrivilege: RoleEnum.ADMINISTRATOR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
newPrivilege: RoleEnum.DEFAULT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
newPrivilege: RoleEnum.DEFAULT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
newPrivilege: RoleEnum.DEFAULT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
newPrivilege: RoleEnum.DEFAULT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-privilege',
|
||||||
|
conversationId: CONTACT_A,
|
||||||
|
newPrivilege: RoleEnum.DEFAULT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Pending Add - one', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-one',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-one',
|
||||||
|
conversationId: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_B,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Pending Add - many', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
.add('Pending Remove - one', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: INVITEE_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: INVITEE_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: INVITEE_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: CONTACT_B,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_C,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: CONTACT_B,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: CONTACT_B,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
inviter: CONTACT_B,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_B,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
conversationId: INVITEE_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Pending Remove - many', () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: OUR_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ID,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
60
ts/components/conversation/GroupV2Change.tsx
Normal file
60
ts/components/conversation/GroupV2Change.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ReplacementValuesType } from '../../types/I18N';
|
||||||
|
import { FullJSXType, Intl } from '../Intl';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
import { GroupV2ChangeType } from '../../groups';
|
||||||
|
|
||||||
|
import { renderChange, SmartContactRendererType } from '../../groupChange';
|
||||||
|
|
||||||
|
import { AccessControlClass, MemberClass } from '../../textsecure.d';
|
||||||
|
|
||||||
|
export type PropsDataType = {
|
||||||
|
ourConversationId: string;
|
||||||
|
change: GroupV2ChangeType;
|
||||||
|
AccessControlEnum: typeof AccessControlClass.AccessRequired;
|
||||||
|
RoleEnum: typeof MemberClass.Role;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsHousekeepingType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
renderContact: SmartContactRendererType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsType = PropsDataType & PropsHousekeepingType;
|
||||||
|
|
||||||
|
function renderStringToIntl(
|
||||||
|
id: string,
|
||||||
|
i18n: LocalizerType,
|
||||||
|
components?: Array<FullJSXType> | ReplacementValuesType<FullJSXType>
|
||||||
|
): FullJSXType {
|
||||||
|
return <Intl id={id} i18n={i18n} components={components} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupV2Change(props: PropsType): React.ReactElement {
|
||||||
|
const {
|
||||||
|
AccessControlEnum,
|
||||||
|
change,
|
||||||
|
i18n,
|
||||||
|
ourConversationId,
|
||||||
|
renderContact,
|
||||||
|
RoleEnum,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-group-v2-change">
|
||||||
|
<div className="module-group-v2-change--icon" />
|
||||||
|
{renderChange(change, {
|
||||||
|
AccessControlEnum,
|
||||||
|
i18n,
|
||||||
|
ourConversationId,
|
||||||
|
renderContact,
|
||||||
|
renderString: renderStringToIntl,
|
||||||
|
RoleEnum,
|
||||||
|
}).map((item: FullJSXType, index: number) => (
|
||||||
|
<div key={index}>{item}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -256,6 +256,7 @@ const renderItem = (id: string) => (
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
conversationId=""
|
conversationId=""
|
||||||
conversationAccepted
|
conversationAccepted
|
||||||
|
renderContact={() => '*ContactName*'}
|
||||||
{...actions()}
|
{...actions()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,6 +28,10 @@ const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderContact = (conversationId: string) => (
|
||||||
|
<React.Fragment key={conversationId}>{conversationId}</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
const getDefaultProps = () => ({
|
const getDefaultProps = () => ({
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationAccepted: true,
|
conversationAccepted: true,
|
||||||
|
@ -55,6 +59,8 @@ const getDefaultProps = () => ({
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
downloadNewVersion: action('downloadNewVersion'),
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
showIdentity: action('showIdentity'),
|
showIdentity: action('showIdentity'),
|
||||||
|
|
||||||
|
renderContact,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,11 @@ import {
|
||||||
GroupNotification,
|
GroupNotification,
|
||||||
PropsData as GroupNotificationProps,
|
PropsData as GroupNotificationProps,
|
||||||
} from './GroupNotification';
|
} from './GroupNotification';
|
||||||
|
import {
|
||||||
|
GroupV2Change,
|
||||||
|
PropsDataType as GroupV2ChangeProps,
|
||||||
|
} from './GroupV2Change';
|
||||||
|
import { SmartContactRendererType } from '../../groupChange';
|
||||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||||
import {
|
import {
|
||||||
ProfileChangeNotification,
|
ProfileChangeNotification,
|
||||||
|
@ -73,6 +78,10 @@ type GroupNotificationType = {
|
||||||
type: 'groupNotification';
|
type: 'groupNotification';
|
||||||
data: GroupNotificationProps;
|
data: GroupNotificationProps;
|
||||||
};
|
};
|
||||||
|
type GroupV2ChangeType = {
|
||||||
|
type: 'groupV2Change';
|
||||||
|
data: GroupV2ChangeProps;
|
||||||
|
};
|
||||||
type ResetSessionNotificationType = {
|
type ResetSessionNotificationType = {
|
||||||
type: 'resetSessionNotification';
|
type: 'resetSessionNotification';
|
||||||
data: null;
|
data: null;
|
||||||
|
@ -85,6 +94,7 @@ type ProfileChangeNotificationType = {
|
||||||
export type TimelineItemType =
|
export type TimelineItemType =
|
||||||
| CallHistoryType
|
| CallHistoryType
|
||||||
| GroupNotificationType
|
| GroupNotificationType
|
||||||
|
| GroupV2ChangeType
|
||||||
| LinkNotificationType
|
| LinkNotificationType
|
||||||
| MessageType
|
| MessageType
|
||||||
| ProfileChangeNotificationType
|
| ProfileChangeNotificationType
|
||||||
|
@ -101,6 +111,7 @@ type PropsLocalType = {
|
||||||
id: string;
|
id: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
|
renderContact: SmartContactRendererType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -120,6 +131,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
isSelected,
|
isSelected,
|
||||||
item,
|
item,
|
||||||
i18n,
|
i18n,
|
||||||
|
renderContact,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -165,6 +177,14 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
notification = (
|
notification = (
|
||||||
<GroupNotification {...this.props} {...item.data} i18n={i18n} />
|
<GroupNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
|
} else if (item.type === 'groupV2Change') {
|
||||||
|
notification = (
|
||||||
|
<GroupV2Change
|
||||||
|
renderContact={renderContact}
|
||||||
|
{...item.data}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (item.type === 'resetSessionNotification') {
|
} else if (item.type === 'resetSessionNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<ResetSessionNotification {...this.props} {...item.data} i18n={i18n} />
|
<ResetSessionNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
|
@ -174,7 +194,12 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
<ProfileChangeNotification {...this.props} {...item.data} i18n={i18n} />
|
<ProfileChangeNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('TimelineItem: Unknown type!');
|
// Weird, yes, but the idea is to get a compile error when we aren't comprehensive
|
||||||
|
// with our if/else checks above, but also log out the type we don't understand if
|
||||||
|
// we encounter it at runtime.
|
||||||
|
const unknownItem: never = item;
|
||||||
|
const asItem = unknownItem as TimelineItemType;
|
||||||
|
throw new Error(`TimelineItem: Unknown type: ${asItem.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Intl } from '../Intl';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
type: 'fromOther' | 'fromMe' | 'fromSync';
|
type: 'fromOther' | 'fromMe' | 'fromSync' | 'fromMember';
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -66,6 +66,10 @@ export class TimerNotification extends React.Component<Props> {
|
||||||
return disabled
|
return disabled
|
||||||
? i18n('disappearingMessagesDisabled')
|
? i18n('disappearingMessagesDisabled')
|
||||||
: i18n('timerSetOnSync', [timespan]);
|
: i18n('timerSetOnSync', [timespan]);
|
||||||
|
case 'fromMember':
|
||||||
|
return disabled
|
||||||
|
? i18n('disappearingMessagesDisabledByMember')
|
||||||
|
: i18n('timerSetByMember', [timespan]);
|
||||||
default:
|
default:
|
||||||
console.warn('TimerNotification: unsupported type provided:', type);
|
console.warn('TimerNotification: unsupported type provided:', type);
|
||||||
|
|
||||||
|
|
536
ts/groupChange.ts
Normal file
536
ts/groupChange.ts
Normal file
|
@ -0,0 +1,536 @@
|
||||||
|
import { FullJSXType } from './components/Intl';
|
||||||
|
import { LocalizerType } from './types/Util';
|
||||||
|
import { ReplacementValuesType } from './types/I18N';
|
||||||
|
import { missingCaseError } from './util/missingCaseError';
|
||||||
|
|
||||||
|
import { AccessControlClass, MemberClass } from './textsecure.d';
|
||||||
|
import { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups';
|
||||||
|
|
||||||
|
export type SmartContactRendererType = (conversationId: string) => FullJSXType;
|
||||||
|
export type StringRendererType = (
|
||||||
|
id: string,
|
||||||
|
i18n: LocalizerType,
|
||||||
|
components?: Array<FullJSXType> | ReplacementValuesType<FullJSXType>
|
||||||
|
) => FullJSXType;
|
||||||
|
|
||||||
|
export type RenderOptionsType = {
|
||||||
|
AccessControlEnum: typeof AccessControlClass.AccessRequired;
|
||||||
|
from?: string;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
ourConversationId: string;
|
||||||
|
renderContact: SmartContactRendererType;
|
||||||
|
renderString: StringRendererType;
|
||||||
|
RoleEnum: typeof MemberClass.Role;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderChange(
|
||||||
|
change: GroupV2ChangeType,
|
||||||
|
options: RenderOptionsType
|
||||||
|
) {
|
||||||
|
const { details, from } = change;
|
||||||
|
|
||||||
|
return details.map((detail: GroupV2ChangeDetailType) =>
|
||||||
|
renderChangeDetail(detail, {
|
||||||
|
...options,
|
||||||
|
from,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
||||||
|
export function renderChangeDetail(
|
||||||
|
detail: GroupV2ChangeDetailType,
|
||||||
|
options: RenderOptionsType
|
||||||
|
): FullJSXType {
|
||||||
|
const {
|
||||||
|
AccessControlEnum,
|
||||||
|
from,
|
||||||
|
i18n,
|
||||||
|
ourConversationId,
|
||||||
|
renderContact,
|
||||||
|
renderString,
|
||||||
|
RoleEnum,
|
||||||
|
} = options;
|
||||||
|
const fromYou = Boolean(from && from === ourConversationId);
|
||||||
|
|
||||||
|
if (detail.type === 'title') {
|
||||||
|
const { newTitle } = detail;
|
||||||
|
|
||||||
|
if (newTitle) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--title--change--you', i18n, [newTitle]);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--title--change--other', i18n, {
|
||||||
|
memberName: renderContact(from),
|
||||||
|
newTitle,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--title--change--unknown', i18n, [
|
||||||
|
newTitle,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--title--remove--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--title--remove--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--title--remove--unknown', i18n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'avatar') {
|
||||||
|
if (detail.removed) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--avatar--remove--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--avatar--remove--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--avatar--remove--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--avatar--change--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--avatar--change--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--avatar--change--unknown', i18n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'access-attributes') {
|
||||||
|
const { newPrivilege } = detail;
|
||||||
|
|
||||||
|
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--access-attributes--admins--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--access-attributes--admins--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--access-attributes--admins--unknown',
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (newPrivilege === AccessControlEnum.MEMBER) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--access-attributes--all--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--access-attributes--all--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--access-attributes--all--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`access-attributes change type, privilege ${newPrivilege} is unknown`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'access-members') {
|
||||||
|
const { newPrivilege } = detail;
|
||||||
|
|
||||||
|
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--access-members--admins--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--access-members--admins--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--access-members--admins--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else if (newPrivilege === AccessControlEnum.MEMBER) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--access-members--all--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--access-members--all--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--access-members--all--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`access-members change type, privilege ${newPrivilege} is unknown`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'member-add') {
|
||||||
|
const { conversationId } = detail;
|
||||||
|
const weAreJoiner = conversationId === ourConversationId;
|
||||||
|
|
||||||
|
if (weAreJoiner) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--member-add--you--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--member-add--you--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--member-add--you--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--member-add--other--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--member-add--other--other', i18n, {
|
||||||
|
adderName: renderContact(from),
|
||||||
|
addeeName: renderContact(conversationId),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--member-add--other--unknown', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'member-add-from-invite') {
|
||||||
|
const { conversationId, inviter } = detail;
|
||||||
|
const weAreJoiner = conversationId === ourConversationId;
|
||||||
|
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
||||||
|
|
||||||
|
if (weAreJoiner) {
|
||||||
|
return renderString('GroupV2--member-add--from-invite--you', i18n, [
|
||||||
|
renderContact(inviter),
|
||||||
|
]);
|
||||||
|
} else if (weAreInviter) {
|
||||||
|
return renderString('GroupV2--member-add--from-invite--from-you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--member-add--from-invite--other', i18n, {
|
||||||
|
inviteeName: renderContact(conversationId),
|
||||||
|
inviterName: renderContact(inviter),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'member-remove') {
|
||||||
|
const { conversationId } = detail;
|
||||||
|
const weAreLeaver = conversationId === ourConversationId;
|
||||||
|
|
||||||
|
if (weAreLeaver) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--member-remove--you--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--member-remove--you--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--member-remove--you--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--member-remove--other--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
} else if (from && from === conversationId) {
|
||||||
|
return renderString('GroupV2--member-remove--other--self', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--member-remove--other--other', i18n, {
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(conversationId),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--member-remove--other--unknown', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'member-privilege') {
|
||||||
|
const { conversationId, newPrivilege } = detail;
|
||||||
|
const weAreMember = conversationId === ourConversationId;
|
||||||
|
|
||||||
|
if (newPrivilege === RoleEnum.ADMINISTRATOR) {
|
||||||
|
if (weAreMember) {
|
||||||
|
if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--you--other',
|
||||||
|
i18n,
|
||||||
|
[renderContact(from)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--you--unknown',
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--other--you',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--other--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(conversationId),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--other--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (newPrivilege === RoleEnum.DEFAULT) {
|
||||||
|
if (weAreMember) {
|
||||||
|
if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--you--other',
|
||||||
|
i18n,
|
||||||
|
[renderContact(from)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--you--unknown',
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--other--you',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--other--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(conversationId),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--other--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`member-privilege change type, privilege ${newPrivilege} is unknown`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'pending-add-one') {
|
||||||
|
const { conversationId } = detail;
|
||||||
|
const weAreInvited = conversationId === ourConversationId;
|
||||||
|
if (weAreInvited) {
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--pending-add--one--you--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--pending-add--one--you--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--pending-add--one--other--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--pending-add--one--other--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--pending-add--one--other--unknown', i18n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'pending-add-many') {
|
||||||
|
const { count } = detail;
|
||||||
|
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--pending-add--many--you', i18n, [
|
||||||
|
count.toString(),
|
||||||
|
]);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString('GroupV2--pending-add--many--other', i18n, {
|
||||||
|
memberName: renderContact(from),
|
||||||
|
count: count.toString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--pending-add--many--unknown', i18n, [
|
||||||
|
count.toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'pending-remove-one') {
|
||||||
|
const { inviter, conversationId } = detail;
|
||||||
|
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
||||||
|
const sentByInvited = Boolean(from && from === conversationId);
|
||||||
|
|
||||||
|
if (weAreInviter) {
|
||||||
|
if (inviter && sentByInvited) {
|
||||||
|
return renderString('GroupV2--pending-remove--decline--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
} else if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--one--you',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--one--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
inviteeName: renderContact(conversationId),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (sentByInvited) {
|
||||||
|
if (inviter) {
|
||||||
|
return renderString('GroupV2--pending-remove--decline--other', i18n, [
|
||||||
|
renderContact(inviter),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return renderString('GroupV2--pending-remove--decline--unknown', i18n);
|
||||||
|
}
|
||||||
|
} else if (inviter) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--one--you',
|
||||||
|
i18n,
|
||||||
|
[renderContact(inviter)]
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--one--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(inviter),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--one--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(inviter)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--pending-remove--revoke--one--you', i18n);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--one--other',
|
||||||
|
i18n,
|
||||||
|
[renderContact(from)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--one--unknown',
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (detail.type === 'pending-remove-many') {
|
||||||
|
const { count, inviter } = detail;
|
||||||
|
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
||||||
|
|
||||||
|
if (weAreInviter) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--many--you',
|
||||||
|
i18n,
|
||||||
|
[count.toString()]
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--many--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
count: count.toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
|
||||||
|
i18n,
|
||||||
|
[count.toString()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (inviter) {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--many--you',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
count: count.toString(),
|
||||||
|
memberName: renderContact(inviter),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--many--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
count: count.toString(),
|
||||||
|
memberName: renderContact(inviter),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--many--unknown',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
count: count.toString(),
|
||||||
|
memberName: renderContact(inviter),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--many--you',
|
||||||
|
i18n,
|
||||||
|
[count.toString()]
|
||||||
|
);
|
||||||
|
} else if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--many--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
memberName: renderContact(from),
|
||||||
|
count: count.toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--many--unknown',
|
||||||
|
i18n,
|
||||||
|
[count.toString()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(detail);
|
||||||
|
}
|
||||||
|
}
|
2296
ts/groups.ts
Normal file
2296
ts/groups.ts
Normal file
File diff suppressed because it is too large
Load diff
88
ts/model-types.d.ts
vendored
88
ts/model-types.d.ts
vendored
|
@ -1,5 +1,6 @@
|
||||||
import * as Backbone from 'backbone';
|
import * as Backbone from 'backbone';
|
||||||
|
|
||||||
|
import { GroupV2ChangeType } from './groups';
|
||||||
import { LocalizerType } from './types/Util';
|
import { LocalizerType } from './types/Util';
|
||||||
import { CallHistoryDetailsType } from './types/Calling';
|
import { CallHistoryDetailsType } from './types/Calling';
|
||||||
import { ColorType } from './types/Colors';
|
import { ColorType } from './types/Colors';
|
||||||
|
@ -26,7 +27,24 @@ type TaskResultType = any;
|
||||||
|
|
||||||
type MessageAttributesType = {
|
type MessageAttributesType = {
|
||||||
id: string;
|
id: string;
|
||||||
serverTimestamp: number;
|
type?: string;
|
||||||
|
|
||||||
|
expirationTimerUpdate?: {
|
||||||
|
expireTimer: number;
|
||||||
|
source?: string;
|
||||||
|
sourceUuid?: string;
|
||||||
|
};
|
||||||
|
// Legacy fields for timer update notification only
|
||||||
|
flags?: number;
|
||||||
|
groupV2Change?: GroupV2ChangeType;
|
||||||
|
// Required. Used to sort messages in the database for the conversation timeline.
|
||||||
|
received_at?: number;
|
||||||
|
// More of a legacy feature, needed as we were updating the schema of messages in the
|
||||||
|
// background, when we were still in IndexedDB, before attachments had gone to disk
|
||||||
|
// We set this so that the idle message upgrade process doesn't pick this message up
|
||||||
|
schemaVersion: number;
|
||||||
|
serverTimestamp?: number;
|
||||||
|
sourceUuid?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare class MessageModelType extends Backbone.Model<MessageAttributesType> {
|
declare class MessageModelType extends Backbone.Model<MessageAttributesType> {
|
||||||
|
@ -49,27 +67,71 @@ type ConversationTypeType = 'private' | 'group';
|
||||||
|
|
||||||
type ConversationAttributesType = {
|
type ConversationAttributesType = {
|
||||||
id: string;
|
id: string;
|
||||||
uuid?: string;
|
type: ConversationTypeType;
|
||||||
e164?: string;
|
timestamp: number;
|
||||||
|
|
||||||
|
// Shared fields
|
||||||
active_at?: number | null;
|
active_at?: number | null;
|
||||||
draft?: string;
|
draft?: string;
|
||||||
groupId?: string;
|
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
lastMessage?: string;
|
lastMessage?: string;
|
||||||
members?: Array<string>;
|
name?: string;
|
||||||
needsStorageServiceSync?: boolean;
|
needsStorageServiceSync?: boolean;
|
||||||
needsVerification?: boolean;
|
needsVerification?: boolean;
|
||||||
profileFamilyName?: string | null;
|
|
||||||
profileKey?: string | null;
|
|
||||||
profileName?: string | null;
|
|
||||||
profileSharing: boolean;
|
profileSharing: boolean;
|
||||||
storageID?: string;
|
storageID?: string;
|
||||||
storageUnknownFields: string;
|
storageUnknownFields: string;
|
||||||
type: ConversationTypeType;
|
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
verified?: number;
|
|
||||||
version: number;
|
version: number;
|
||||||
|
|
||||||
|
// Private core info
|
||||||
|
uuid?: string;
|
||||||
|
e164?: string;
|
||||||
|
|
||||||
|
// Private other fields
|
||||||
|
profileFamilyName?: string | null;
|
||||||
|
profileKey?: string | null;
|
||||||
|
profileName?: string | null;
|
||||||
|
verified?: number;
|
||||||
|
|
||||||
|
// Group-only
|
||||||
|
groupId?: string;
|
||||||
|
left: boolean;
|
||||||
|
groupVersion?: number;
|
||||||
|
|
||||||
|
// GroupV1 only
|
||||||
|
members?: Array<string>;
|
||||||
|
|
||||||
|
// GroupV2 core info
|
||||||
|
masterKey?: string;
|
||||||
|
secretParams?: string;
|
||||||
|
publicParams?: string;
|
||||||
|
revision?: number;
|
||||||
|
|
||||||
|
// GroupV2 other fields
|
||||||
|
accessControl?: {
|
||||||
|
attributes: number;
|
||||||
|
members: number;
|
||||||
|
};
|
||||||
|
avatar?: {
|
||||||
|
url: string;
|
||||||
|
path: string;
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
expireTimer?: number;
|
||||||
|
membersV2?: Array<GroupV2MemberType>;
|
||||||
|
pendingMembersV2?: Array<GroupV2PendingMemberType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupV2MemberType = {
|
||||||
|
conversationId: string;
|
||||||
|
role: number;
|
||||||
|
joinedAtVersion: number;
|
||||||
|
};
|
||||||
|
export type GroupV2PendingMemberType = {
|
||||||
|
addedByUserId: string;
|
||||||
|
conversationId: string;
|
||||||
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VerificationOptions = {
|
type VerificationOptions = {
|
||||||
|
@ -113,6 +175,12 @@ export declare class ConversationModelType extends Backbone.Model<
|
||||||
isMe(): boolean;
|
isMe(): boolean;
|
||||||
isPrivate(): boolean;
|
isPrivate(): boolean;
|
||||||
isVerified(): boolean;
|
isVerified(): boolean;
|
||||||
|
maybeRepairGroupV2(data: {
|
||||||
|
masterKey: string;
|
||||||
|
secretParams: string;
|
||||||
|
publicParams: string;
|
||||||
|
}): void;
|
||||||
|
queueJob(job: () => Promise<void>): Promise<void>;
|
||||||
safeGetVerified(): Promise<number>;
|
safeGetVerified(): Promise<number>;
|
||||||
setArchived(isArchived: boolean): void;
|
setArchived(isArchived: boolean): void;
|
||||||
setProfileKey(
|
setProfileKey(
|
||||||
|
|
212
ts/services/groupCredentialFetcher.ts
Normal file
212
ts/services/groupCredentialFetcher.ts
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
import { last, sortBy } from 'lodash';
|
||||||
|
import { AuthCredentialResponse } from 'zkgroup';
|
||||||
|
|
||||||
|
import {
|
||||||
|
base64ToCompatArray,
|
||||||
|
compatArrayToBase64,
|
||||||
|
getClientZkAuthOperations,
|
||||||
|
} from '../util/zkgroup';
|
||||||
|
|
||||||
|
import { GroupCredentialType } from '../textsecure/WebAPI';
|
||||||
|
|
||||||
|
export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
|
||||||
|
|
||||||
|
type CredentialsDataType = Array<GroupCredentialType>;
|
||||||
|
type RequestDatesType = {
|
||||||
|
startDay: number;
|
||||||
|
endDay: number;
|
||||||
|
};
|
||||||
|
type NextCredentialsType = {
|
||||||
|
today: GroupCredentialType;
|
||||||
|
tomorrow: GroupCredentialType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECOND = 1000;
|
||||||
|
const MINUTE = 60 * SECOND;
|
||||||
|
const HOUR = 60 * MINUTE;
|
||||||
|
const DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
function getTodayInEpoch() {
|
||||||
|
return Math.floor(Date.now() / DAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sleep(ms: number) {
|
||||||
|
// tslint:disable-next-line no-string-based-set-timeout
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
export async function initializeGroupCredentialFetcher(): Promise<void> {
|
||||||
|
if (started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info('initializeGroupCredentialFetcher: starting...');
|
||||||
|
started = true;
|
||||||
|
|
||||||
|
// Because we fetch eight days of credentials at a time, we really only need to run
|
||||||
|
// this about once a week. But there's no problem running it more often; it will do
|
||||||
|
// nothing if no new credentials are needed, and will only request needed credentials.
|
||||||
|
await runWithRetry(maybeFetchNewCredentials, { scheduleAnother: 4 * HOUR });
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackoffType = {
|
||||||
|
[key: number]: number | undefined;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
const BACKOFF: BackoffType = {
|
||||||
|
0: SECOND,
|
||||||
|
1: 5 * SECOND,
|
||||||
|
2: 30 * SECOND,
|
||||||
|
3: 2 * MINUTE,
|
||||||
|
max: 5 * MINUTE,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runWithRetry(
|
||||||
|
fn: () => Promise<void>,
|
||||||
|
options: { scheduleAnother?: number } = {}
|
||||||
|
): Promise<void> {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
count += 1;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await fn();
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
const wait = BACKOFF[count] || BACKOFF.max;
|
||||||
|
window.log.info(
|
||||||
|
`runWithRetry: ${fn.name} failed. Waiting ${wait}ms for retry. Error: ${error.stack}`
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep(wait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's important to schedule our next run here instead of the level above; otherwise we
|
||||||
|
// could end up with multiple endlessly-retrying runs.
|
||||||
|
const duration = options.scheduleAnother;
|
||||||
|
if (duration) {
|
||||||
|
window.log.info(
|
||||||
|
`runWithRetry: scheduling another run with a setTimeout duration of ${duration}ms`
|
||||||
|
);
|
||||||
|
setTimeout(async () => runWithRetry(fn, options), duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In cases where we are at a day boundary, we might need to use tomorrow in a retry
|
||||||
|
export function getCredentialsForToday(
|
||||||
|
data: CredentialsDataType | undefined
|
||||||
|
): NextCredentialsType {
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('getCredentialsForToday: No credentials fetched!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const todayInEpoch = getTodayInEpoch();
|
||||||
|
const todayIndex = data.findIndex(
|
||||||
|
(item: GroupCredentialType) => item.redemptionTime === todayInEpoch
|
||||||
|
);
|
||||||
|
if (todayIndex < 0) {
|
||||||
|
throw new Error(
|
||||||
|
'getCredentialsForToday: Cannot find credentials for today'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
today: data[todayIndex],
|
||||||
|
tomorrow: data[todayIndex + 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function maybeFetchNewCredentials(): Promise<void> {
|
||||||
|
const uuid = window.textsecure.storage.user.getUuid();
|
||||||
|
if (!uuid) {
|
||||||
|
window.log.info('maybeFetchCredentials: no UUID, returning early');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous: CredentialsDataType | undefined = window.storage.get(
|
||||||
|
GROUP_CREDENTIALS_KEY
|
||||||
|
);
|
||||||
|
const requestDates = getDatesForRequest(previous);
|
||||||
|
if (!requestDates) {
|
||||||
|
window.log.info('maybeFetchCredentials: no new credentials needed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountManager = window.getAccountManager();
|
||||||
|
if (!accountManager) {
|
||||||
|
window.log.info('maybeFetchCredentials: unable to get AccountManager');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startDay, endDay } = requestDates;
|
||||||
|
window.log.info(
|
||||||
|
`maybeFetchCredentials: fetching credentials for ${startDay} through ${endDay}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverPublicParamsBase64 = window.getServerPublicParams();
|
||||||
|
const clientZKAuthOperations = getClientZkAuthOperations(
|
||||||
|
serverPublicParamsBase64
|
||||||
|
);
|
||||||
|
const newCredentials = sortCredentials(
|
||||||
|
await accountManager.getGroupCredentials(startDay, endDay)
|
||||||
|
).map((item: GroupCredentialType) => {
|
||||||
|
const authCredential = clientZKAuthOperations.receiveAuthCredential(
|
||||||
|
uuid,
|
||||||
|
item.redemptionTime,
|
||||||
|
new AuthCredentialResponse(base64ToCompatArray(item.credential))
|
||||||
|
);
|
||||||
|
const credential = compatArrayToBase64(authCredential.serialize());
|
||||||
|
|
||||||
|
return {
|
||||||
|
redemptionTime: item.redemptionTime,
|
||||||
|
credential,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const todayInEpoch = getTodayInEpoch();
|
||||||
|
const previousCleaned = previous
|
||||||
|
? previous.filter(
|
||||||
|
(item: GroupCredentialType) => item.redemptionTime >= todayInEpoch
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
const finalCredentials = [...previousCleaned, ...newCredentials];
|
||||||
|
|
||||||
|
window.log.info('maybeFetchCredentials: Saving new credentials...');
|
||||||
|
// Note: we don't wait for this to finish
|
||||||
|
window.storage.put(GROUP_CREDENTIALS_KEY, finalCredentials);
|
||||||
|
window.log.info('maybeFetchCredentials: Save complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDatesForRequest(
|
||||||
|
data?: CredentialsDataType
|
||||||
|
): RequestDatesType | undefined {
|
||||||
|
const todayInEpoch = getTodayInEpoch();
|
||||||
|
const oneWeekOut = todayInEpoch + 7;
|
||||||
|
|
||||||
|
const lastCredential = last(data);
|
||||||
|
if (!lastCredential || lastCredential.redemptionTime < todayInEpoch) {
|
||||||
|
return {
|
||||||
|
startDay: todayInEpoch,
|
||||||
|
endDay: oneWeekOut,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastCredential.redemptionTime >= oneWeekOut) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDay: lastCredential.redemptionTime + 1,
|
||||||
|
endDay: oneWeekOut,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortCredentials(
|
||||||
|
data: CredentialsDataType
|
||||||
|
): CredentialsDataType {
|
||||||
|
return sortBy(data, (item: GroupCredentialType) => item.redemptionTime);
|
||||||
|
}
|
|
@ -22,9 +22,11 @@ import {
|
||||||
mergeAccountRecord,
|
mergeAccountRecord,
|
||||||
mergeContactRecord,
|
mergeContactRecord,
|
||||||
mergeGroupV1Record,
|
mergeGroupV1Record,
|
||||||
|
mergeGroupV2Record,
|
||||||
toAccountRecord,
|
toAccountRecord,
|
||||||
toContactRecord,
|
toContactRecord,
|
||||||
toGroupV1Record,
|
toGroupV1Record,
|
||||||
|
toGroupV2Record,
|
||||||
} from './storageRecordOps';
|
} from './storageRecordOps';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -128,6 +130,11 @@ async function generateManifest(
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
storageRecord.contact = await toContactRecord(conversation);
|
storageRecord.contact = await toContactRecord(conversation);
|
||||||
identifier.type = ITEM_TYPE.CONTACT;
|
identifier.type = ITEM_TYPE.CONTACT;
|
||||||
|
} else if ((conversation.get('groupVersion') || 0) > 1) {
|
||||||
|
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
storageRecord.groupV1 = await toGroupV2Record(conversation);
|
||||||
|
identifier.type = ITEM_TYPE.GROUPV2;
|
||||||
} else {
|
} else {
|
||||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
@ -389,7 +396,8 @@ async function fetchManifest(
|
||||||
if (err.code === 404) {
|
if (err.code === 404) {
|
||||||
await createNewManifest();
|
await createNewManifest();
|
||||||
return;
|
return;
|
||||||
} else if (err.code === 204) {
|
}
|
||||||
|
if (err.code === 204) {
|
||||||
// noNewerManifest we're ok
|
// noNewerManifest we're ok
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -429,6 +437,12 @@ async function mergeRecord(
|
||||||
hasConflict = await mergeContactRecord(storageID, storageRecord.contact);
|
hasConflict = await mergeContactRecord(storageID, storageRecord.contact);
|
||||||
} else if (itemType === ITEM_TYPE.GROUPV1 && storageRecord.groupV1) {
|
} else if (itemType === ITEM_TYPE.GROUPV1 && storageRecord.groupV1) {
|
||||||
hasConflict = await mergeGroupV1Record(storageID, storageRecord.groupV1);
|
hasConflict = await mergeGroupV1Record(storageID, storageRecord.groupV1);
|
||||||
|
} else if (
|
||||||
|
window.GV2 &&
|
||||||
|
itemType === ITEM_TYPE.GROUPV2 &&
|
||||||
|
storageRecord.groupV2
|
||||||
|
) {
|
||||||
|
hasConflict = await mergeGroupV2Record(storageID, storageRecord.groupV2);
|
||||||
} else if (itemType === ITEM_TYPE.ACCOUNT && storageRecord.account) {
|
} else if (itemType === ITEM_TYPE.ACCOUNT && storageRecord.account) {
|
||||||
hasConflict = await mergeAccountRecord(storageID, storageRecord.account);
|
hasConflict = await mergeAccountRecord(storageID, storageRecord.account);
|
||||||
} else {
|
} else {
|
||||||
|
@ -592,9 +606,9 @@ async function processManifest(
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
consecutiveConflicts = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consecutiveConflicts = 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`storageService.processManifest: failed! ${
|
`storageService.processManifest: failed! ${
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* tslint:disable no-backbone-get-set-outside-model */
|
/* tslint:disable no-backbone-get-set-outside-model */
|
||||||
import _ from 'lodash';
|
import { isEqual, isNumber } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
|
@ -11,12 +11,18 @@ import {
|
||||||
AccountRecordClass,
|
AccountRecordClass,
|
||||||
ContactRecordClass,
|
ContactRecordClass,
|
||||||
GroupV1RecordClass,
|
GroupV1RecordClass,
|
||||||
|
GroupV2RecordClass,
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
|
import { deriveGroupFields, waitThenMaybeUpdateGroup } from '../groups';
|
||||||
import { ConversationModelType } from '../model-types.d';
|
import { ConversationModelType } from '../model-types.d';
|
||||||
|
|
||||||
const { updateConversation } = dataInterface;
|
const { updateConversation } = dataInterface;
|
||||||
|
|
||||||
type RecordClass = AccountRecordClass | ContactRecordClass | GroupV1RecordClass;
|
type RecordClass =
|
||||||
|
| AccountRecordClass
|
||||||
|
| ContactRecordClass
|
||||||
|
| GroupV1RecordClass
|
||||||
|
| GroupV2RecordClass;
|
||||||
|
|
||||||
function toRecordVerified(verified: number): number {
|
function toRecordVerified(verified: number): number {
|
||||||
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
||||||
|
@ -147,6 +153,24 @@ export async function toGroupV1Record(
|
||||||
return groupV1Record;
|
return groupV1Record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function toGroupV2Record(
|
||||||
|
conversation: ConversationModelType
|
||||||
|
): Promise<GroupV2RecordClass> {
|
||||||
|
const groupV2Record = new window.textsecure.protobuf.GroupV2Record();
|
||||||
|
|
||||||
|
const masterKey = conversation.get('masterKey');
|
||||||
|
if (masterKey !== undefined) {
|
||||||
|
groupV2Record.masterKey = base64ToArrayBuffer(masterKey);
|
||||||
|
}
|
||||||
|
groupV2Record.blocked = conversation.isBlocked();
|
||||||
|
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
|
groupV2Record.archived = Boolean(conversation.get('isArchived'));
|
||||||
|
|
||||||
|
applyUnknownFields(groupV2Record, conversation);
|
||||||
|
|
||||||
|
return groupV2Record;
|
||||||
|
}
|
||||||
|
|
||||||
type MessageRequestCapableRecord = ContactRecordClass | GroupV1RecordClass;
|
type MessageRequestCapableRecord = ContactRecordClass | GroupV1RecordClass;
|
||||||
|
|
||||||
function applyMessageRequestState(
|
function applyMessageRequestState(
|
||||||
|
@ -183,7 +207,7 @@ function doesRecordHavePendingChanges(
|
||||||
): boolean {
|
): boolean {
|
||||||
const shouldSync = Boolean(conversation.get('needsStorageServiceSync'));
|
const shouldSync = Boolean(conversation.get('needsStorageServiceSync'));
|
||||||
|
|
||||||
const hasConflict = !_.isEqual(mergedRecord, serviceRecord);
|
const hasConflict = !isEqual(mergedRecord, serviceRecord);
|
||||||
|
|
||||||
if (shouldSync && !hasConflict) {
|
if (shouldSync && !hasConflict) {
|
||||||
conversation.set({ needsStorageServiceSync: false });
|
conversation.set({ needsStorageServiceSync: false });
|
||||||
|
@ -240,6 +264,81 @@ export async function mergeGroupV1Record(
|
||||||
return hasPendingChanges;
|
return hasPendingChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function mergeGroupV2Record(
|
||||||
|
storageID: string,
|
||||||
|
groupV2Record: GroupV2RecordClass
|
||||||
|
): Promise<boolean> {
|
||||||
|
window.log.info(`storageService.mergeGroupV2Record: merging ${storageID}`);
|
||||||
|
|
||||||
|
if (!groupV2Record.masterKey) {
|
||||||
|
window.log.info(
|
||||||
|
`storageService.mergeGroupV2Record: no master key for ${storageID}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const masterKeyBuffer = groupV2Record.masterKey.toArrayBuffer();
|
||||||
|
const groupFields = deriveGroupFields(masterKeyBuffer);
|
||||||
|
|
||||||
|
const groupId = arrayBufferToBase64(groupFields.id);
|
||||||
|
const masterKey = arrayBufferToBase64(masterKeyBuffer);
|
||||||
|
const secretParams = arrayBufferToBase64(groupFields.secretParams);
|
||||||
|
const publicParams = arrayBufferToBase64(groupFields.publicParams);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const conversationId = window.ConversationController.ensureGroup(groupId, {
|
||||||
|
// We want this conversation to show in the left pane when we first learn about it
|
||||||
|
active_at: now,
|
||||||
|
timestamp: now,
|
||||||
|
// Basic GroupV2 data
|
||||||
|
groupVersion: 2,
|
||||||
|
masterKey,
|
||||||
|
secretParams,
|
||||||
|
publicParams,
|
||||||
|
});
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(
|
||||||
|
`storageService.mergeGroupV2Record: No conversation for groupv2(${groupId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.maybeRepairGroupV2({
|
||||||
|
masterKey,
|
||||||
|
secretParams,
|
||||||
|
publicParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
conversation.set({
|
||||||
|
isArchived: Boolean(groupV2Record.archived),
|
||||||
|
storageID,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyMessageRequestState(groupV2Record, conversation);
|
||||||
|
|
||||||
|
addUnknownFields(groupV2Record, conversation);
|
||||||
|
|
||||||
|
const hasPendingChanges = doesRecordHavePendingChanges(
|
||||||
|
await toGroupV2Record(conversation),
|
||||||
|
groupV2Record,
|
||||||
|
conversation
|
||||||
|
);
|
||||||
|
|
||||||
|
updateConversation(conversation.attributes);
|
||||||
|
|
||||||
|
const isFirstSync = !isNumber(window.storage.get('manifestVersion'));
|
||||||
|
const dropInitialJoinMessage = isFirstSync;
|
||||||
|
// tslint:disable-next-line no-floating-promises
|
||||||
|
waitThenMaybeUpdateGroup({
|
||||||
|
conversation,
|
||||||
|
dropInitialJoinMessage,
|
||||||
|
});
|
||||||
|
window.log.info(`storageService.mergeGroupV2Record: merged ${storageID}`);
|
||||||
|
|
||||||
|
return hasPendingChanges;
|
||||||
|
}
|
||||||
|
|
||||||
export async function mergeContactRecord(
|
export async function mergeContactRecord(
|
||||||
storageID: string,
|
storageID: string,
|
||||||
contactRecord: ContactRecordClass
|
contactRecord: ContactRecordClass
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { redactAll } from '../../js/modules/privacy';
|
||||||
import { remove as removeUserConfig } from '../../app/user_config';
|
import { remove as removeUserConfig } from '../../app/user_config';
|
||||||
import { combineNames } from '../util/combineNames';
|
import { combineNames } from '../util/combineNames';
|
||||||
|
|
||||||
|
import { GroupV2MemberType } from '../model-types.d';
|
||||||
import { LocaleMessagesType } from '../types/I18N';
|
import { LocaleMessagesType } from '../types/I18N';
|
||||||
|
|
||||||
import pify from 'pify';
|
import pify from 'pify';
|
||||||
|
@ -2070,6 +2071,7 @@ async function saveConversation(
|
||||||
groupId,
|
groupId,
|
||||||
id,
|
id,
|
||||||
members,
|
members,
|
||||||
|
membersV2,
|
||||||
name,
|
name,
|
||||||
profileFamilyName,
|
profileFamilyName,
|
||||||
profileName,
|
profileName,
|
||||||
|
@ -2077,6 +2079,13 @@ async function saveConversation(
|
||||||
uuid,
|
uuid,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const membersList = membersV2
|
||||||
|
? membersV2.map((item: GroupV2MemberType) => item.conversationId).join(' ')
|
||||||
|
: members
|
||||||
|
? members.join(' ')
|
||||||
|
: null;
|
||||||
|
|
||||||
await instance.run(
|
await instance.run(
|
||||||
`INSERT INTO conversations (
|
`INSERT INTO conversations (
|
||||||
id,
|
id,
|
||||||
|
@ -2119,7 +2128,7 @@ async function saveConversation(
|
||||||
|
|
||||||
$active_at: active_at,
|
$active_at: active_at,
|
||||||
$type: type,
|
$type: type,
|
||||||
$members: members ? members.join(' ') : null,
|
$members: membersList,
|
||||||
$name: name,
|
$name: name,
|
||||||
$profileName: profileName,
|
$profileName: profileName,
|
||||||
$profileFamilyName: profileFamilyName,
|
$profileFamilyName: profileFamilyName,
|
||||||
|
@ -2156,6 +2165,7 @@ async function updateConversation(data: ConversationType) {
|
||||||
active_at,
|
active_at,
|
||||||
type,
|
type,
|
||||||
members,
|
members,
|
||||||
|
membersV2,
|
||||||
name,
|
name,
|
||||||
profileName,
|
profileName,
|
||||||
profileFamilyName,
|
profileFamilyName,
|
||||||
|
@ -2163,6 +2173,13 @@ async function updateConversation(data: ConversationType) {
|
||||||
uuid,
|
uuid,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const membersList = membersV2
|
||||||
|
? membersV2.map((item: GroupV2MemberType) => item.conversationId).join(' ')
|
||||||
|
: members
|
||||||
|
? members.join(' ')
|
||||||
|
: null;
|
||||||
|
|
||||||
await db.run(
|
await db.run(
|
||||||
`UPDATE conversations SET
|
`UPDATE conversations SET
|
||||||
json = $json,
|
json = $json,
|
||||||
|
@ -2187,7 +2204,7 @@ async function updateConversation(data: ConversationType) {
|
||||||
|
|
||||||
$active_at: active_at,
|
$active_at: active_at,
|
||||||
$type: type,
|
$type: type,
|
||||||
$members: members ? members.join(' ') : null,
|
$members: membersList,
|
||||||
$name: name,
|
$name: name,
|
||||||
$profileName: profileName,
|
$profileName: profileName,
|
||||||
$profileFamilyName: profileFamilyName,
|
$profileFamilyName: profileFamilyName,
|
||||||
|
|
|
@ -1009,6 +1009,19 @@ export function reducer(
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let metrics;
|
||||||
|
if (messageIds.length === 0) {
|
||||||
|
metrics = {
|
||||||
|
totalUnread: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
metrics = {
|
||||||
|
...existingConversation.metrics,
|
||||||
|
oldest,
|
||||||
|
newest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
messagesLookup: omit(messagesLookup, id),
|
messagesLookup: omit(messagesLookup, id),
|
||||||
|
@ -1017,11 +1030,7 @@ export function reducer(
|
||||||
...existingConversation,
|
...existingConversation,
|
||||||
messageIds,
|
messageIds,
|
||||||
heightChangeMessageIds,
|
heightChangeMessageIds,
|
||||||
metrics: {
|
metrics,
|
||||||
...existingConversation.metrics,
|
|
||||||
oldest,
|
|
||||||
newest,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
32
ts/state/smart/ContactName.tsx
Normal file
32
ts/state/smart/ContactName.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { ContactName } from '../../components/conversation/ContactName';
|
||||||
|
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import {
|
||||||
|
GetConversationByIdType,
|
||||||
|
getConversationSelector,
|
||||||
|
} from '../selectors/conversations';
|
||||||
|
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
conversationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SmartContactName = (props: ExternalProps) => {
|
||||||
|
const { conversationId } = props;
|
||||||
|
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||||
|
const getConversation = useSelector<StateType, GetConversationByIdType>(
|
||||||
|
getConversationSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversation = getConversation(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Conversation id ${conversationId} not found!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ContactName i18n={i18n} {...conversation} />;
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
@ -10,11 +11,21 @@ import {
|
||||||
getSelectedMessage,
|
getSelectedMessage,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
|
|
||||||
|
import { SmartContactName } from './ContactName';
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
const FilteredSmartContactName = SmartContactName as any;
|
||||||
|
|
||||||
|
function renderContact(conversationId: string): JSX.Element {
|
||||||
|
return <FilteredSmartContactName conversationId={conversationId} />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
const { id, conversationId } = props;
|
const { id, conversationId } = props;
|
||||||
|
|
||||||
|
@ -29,6 +40,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
id,
|
id,
|
||||||
conversationId,
|
conversationId,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
renderContact,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
307
ts/textsecure.d.ts
vendored
307
ts/textsecure.d.ts
vendored
|
@ -6,6 +6,7 @@ import {
|
||||||
} from './libsignal.d';
|
} from './libsignal.d';
|
||||||
import Crypto from './textsecure/Crypto';
|
import Crypto from './textsecure/Crypto';
|
||||||
import MessageReceiver from './textsecure/MessageReceiver';
|
import MessageReceiver from './textsecure/MessageReceiver';
|
||||||
|
import MessageSender from './textsecure/SendMessage';
|
||||||
import EventTarget from './textsecure/EventTarget';
|
import EventTarget from './textsecure/EventTarget';
|
||||||
import { ByteBufferClass } from './window.d';
|
import { ByteBufferClass } from './window.d';
|
||||||
import SendMessage, { SendOptionsType } from './textsecure/SendMessage';
|
import SendMessage, { SendOptionsType } from './textsecure/SendMessage';
|
||||||
|
@ -75,11 +76,7 @@ export type TextSecureType = {
|
||||||
remove: (key: string | Array<string>) => Promise<void>;
|
remove: (key: string | Array<string>) => Promise<void>;
|
||||||
protocol: StorageProtocolType;
|
protocol: StorageProtocolType;
|
||||||
};
|
};
|
||||||
messageReceiver: {
|
messageReceiver: MessageReceiver;
|
||||||
downloadAttachment: (
|
|
||||||
attachment: AttachmentPointerClass
|
|
||||||
) => Promise<DownloadAttachmentType>;
|
|
||||||
};
|
|
||||||
messaging?: SendMessage;
|
messaging?: SendMessage;
|
||||||
protobuf: ProtobufCollectionType;
|
protobuf: ProtobufCollectionType;
|
||||||
utils: typeof utils;
|
utils: typeof utils;
|
||||||
|
@ -145,7 +142,44 @@ export type StorageProtocolType = StorageType & {
|
||||||
|
|
||||||
// Protobufs
|
// Protobufs
|
||||||
|
|
||||||
type StorageServiceProtobufTypes = {
|
type DeviceMessagesProtobufTypes = {
|
||||||
|
ProvisioningUuid: typeof ProvisioningUuidClass;
|
||||||
|
ProvisionEnvelope: typeof ProvisionEnvelopeClass;
|
||||||
|
ProvisionMessage: typeof ProvisionMessageClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeviceNameProtobufTypes = {
|
||||||
|
DeviceName: typeof DeviceNameClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GroupsProtobufTypes = {
|
||||||
|
AvatarUploadAttributes: typeof AvatarUploadAttributesClass;
|
||||||
|
Member: typeof MemberClass;
|
||||||
|
PendingMember: typeof PendingMemberClass;
|
||||||
|
AccessControl: typeof AccessControlClass;
|
||||||
|
Group: typeof GroupClass;
|
||||||
|
GroupChange: typeof GroupChangeClass;
|
||||||
|
GroupChanges: typeof GroupChangesClass;
|
||||||
|
GroupAttributeBlob: typeof GroupAttributeBlobClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SignalServiceProtobufTypes = {
|
||||||
|
AttachmentPointer: typeof AttachmentPointerClass;
|
||||||
|
ContactDetails: typeof ContactDetailsClass;
|
||||||
|
Content: typeof ContentClass;
|
||||||
|
DataMessage: typeof DataMessageClass;
|
||||||
|
Envelope: typeof EnvelopeClass;
|
||||||
|
GroupContext: typeof GroupContextClass;
|
||||||
|
GroupContextV2: typeof GroupContextV2Class;
|
||||||
|
GroupDetails: typeof GroupDetailsClass;
|
||||||
|
NullMessage: typeof NullMessageClass;
|
||||||
|
ReceiptMessage: typeof ReceiptMessageClass;
|
||||||
|
SyncMessage: typeof SyncMessageClass;
|
||||||
|
TypingMessage: typeof TypingMessageClass;
|
||||||
|
Verified: typeof VerifiedClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SignalStorageProtobufTypes = {
|
||||||
AccountRecord: typeof AccountRecordClass;
|
AccountRecord: typeof AccountRecordClass;
|
||||||
ContactRecord: typeof ContactRecordClass;
|
ContactRecord: typeof ContactRecordClass;
|
||||||
GroupV1Record: typeof GroupV1RecordClass;
|
GroupV1Record: typeof GroupV1RecordClass;
|
||||||
|
@ -159,35 +193,252 @@ type StorageServiceProtobufTypes = {
|
||||||
WriteOperation: typeof WriteOperationClass;
|
WriteOperation: typeof WriteOperationClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProtobufCollectionType = StorageServiceProtobufTypes & {
|
type SubProtocolProtobufTypes = {
|
||||||
AttachmentPointer: typeof AttachmentPointerClass;
|
|
||||||
ContactDetails: typeof ContactDetailsClass;
|
|
||||||
Content: typeof ContentClass;
|
|
||||||
DataMessage: typeof DataMessageClass;
|
|
||||||
DeviceName: typeof DeviceNameClass;
|
|
||||||
Envelope: typeof EnvelopeClass;
|
|
||||||
GroupContext: typeof GroupContextClass;
|
|
||||||
GroupContextV2: typeof GroupContextV2Class;
|
|
||||||
GroupDetails: typeof GroupDetailsClass;
|
|
||||||
NullMessage: typeof NullMessageClass;
|
|
||||||
ProvisioningUuid: typeof ProvisioningUuidClass;
|
|
||||||
ProvisionEnvelope: typeof ProvisionEnvelopeClass;
|
|
||||||
ProvisionMessage: typeof ProvisionMessageClass;
|
|
||||||
ReceiptMessage: typeof ReceiptMessageClass;
|
|
||||||
SyncMessage: typeof SyncMessageClass;
|
|
||||||
TypingMessage: typeof TypingMessageClass;
|
|
||||||
Verified: typeof VerifiedClass;
|
|
||||||
WebSocketMessage: typeof WebSocketMessageClass;
|
WebSocketMessage: typeof WebSocketMessageClass;
|
||||||
WebSocketRequestMessage: typeof WebSocketRequestMessageClass;
|
WebSocketRequestMessage: typeof WebSocketRequestMessageClass;
|
||||||
WebSocketResponseMessage: typeof WebSocketResponseMessageClass;
|
WebSocketResponseMessage: typeof WebSocketResponseMessageClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProtobufCollectionType = DeviceMessagesProtobufTypes &
|
||||||
|
DeviceNameProtobufTypes &
|
||||||
|
GroupsProtobufTypes &
|
||||||
|
SignalServiceProtobufTypes &
|
||||||
|
SignalStorageProtobufTypes &
|
||||||
|
SubProtocolProtobufTypes;
|
||||||
|
|
||||||
// Note: there are a lot of places in the code that overwrite a field like this
|
// Note: there are a lot of places in the code that overwrite a field like this
|
||||||
// with a type that the app can use. Being more rigorous with these
|
// with a type that the app can use. Being more rigorous with these
|
||||||
// types would require code changes, out of scope for now.
|
// types would require code changes, out of scope for now.
|
||||||
type ProtoBinaryType = any;
|
type ProtoBinaryType = any;
|
||||||
type ProtoBigNumberType = any;
|
type ProtoBigNumberType = any;
|
||||||
|
|
||||||
|
// Groups.proto
|
||||||
|
|
||||||
|
export declare class AvatarUploadAttributesClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => AvatarUploadAttributesClass;
|
||||||
|
|
||||||
|
key?: string;
|
||||||
|
credential?: string;
|
||||||
|
acl?: string;
|
||||||
|
algorithm?: string;
|
||||||
|
date?: string;
|
||||||
|
policy?: string;
|
||||||
|
signature?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class MemberClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => MemberClass;
|
||||||
|
|
||||||
|
userId?: ProtoBinaryType;
|
||||||
|
role?: MemberRoleEnum;
|
||||||
|
profileKey?: ProtoBinaryType;
|
||||||
|
presentation?: ProtoBinaryType;
|
||||||
|
joinedAtVersion?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemberRoleEnum = number;
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace MemberClass {
|
||||||
|
class Role {
|
||||||
|
static UNKNOWN: number;
|
||||||
|
static DEFAULT: number;
|
||||||
|
static ADMINISTRATOR: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class PendingMemberClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => PendingMemberClass;
|
||||||
|
|
||||||
|
member?: MemberClass;
|
||||||
|
addedByUserId?: ProtoBinaryType;
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessRequiredEnum = number;
|
||||||
|
|
||||||
|
export declare class AccessControlClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => AccessControlClass;
|
||||||
|
|
||||||
|
attributes?: AccessRequiredEnum;
|
||||||
|
members?: AccessRequiredEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace AccessControlClass {
|
||||||
|
class AccessRequired {
|
||||||
|
static UNKNOWN: number;
|
||||||
|
static MEMBER: number;
|
||||||
|
static ADMINISTRATOR: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class GroupClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => GroupClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
publicKey?: ProtoBinaryType;
|
||||||
|
title?: ProtoBinaryType;
|
||||||
|
avatar?: string;
|
||||||
|
disappearingMessagesTimer?: ProtoBinaryType;
|
||||||
|
accessControl?: AccessControlClass;
|
||||||
|
version?: number;
|
||||||
|
members?: Array<MemberClass>;
|
||||||
|
pendingMembers?: Array<PendingMemberClass>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class GroupChangeClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => GroupChangeClass;
|
||||||
|
|
||||||
|
actions?: ProtoBinaryType;
|
||||||
|
serverSignature?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace GroupChangeClass {
|
||||||
|
class Actions {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => Actions;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
sourceUuid?: ProtoBinaryType;
|
||||||
|
version?: number;
|
||||||
|
addMembers?: Array<GroupChangeClass.Actions.AddMemberAction>;
|
||||||
|
deleteMembers?: Array<GroupChangeClass.Actions.DeleteMemberAction>;
|
||||||
|
modifyMemberRoles?: Array<GroupChangeClass.Actions.ModifyMemberRoleAction>;
|
||||||
|
modifyMemberProfileKeys?: Array<
|
||||||
|
GroupChangeClass.Actions.ModifyMemberProfileKeyAction
|
||||||
|
>;
|
||||||
|
addPendingMembers?: Array<GroupChangeClass.Actions.AddPendingMemberAction>;
|
||||||
|
deletePendingMembers?: Array<
|
||||||
|
GroupChangeClass.Actions.DeletePendingMemberAction
|
||||||
|
>;
|
||||||
|
promotePendingMembers?: Array<
|
||||||
|
GroupChangeClass.Actions.PromotePendingMemberAction
|
||||||
|
>;
|
||||||
|
modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction;
|
||||||
|
modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction;
|
||||||
|
modifyDisappearingMessagesTimer?: GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction;
|
||||||
|
modifyAttributesAccess?: GroupChangeClass.Actions.ModifyAttributesAccessControlAction;
|
||||||
|
modifyMemberAccess?: GroupChangeClass.Actions.ModifyMembersAccessControlAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace GroupChangeClass.Actions {
|
||||||
|
class AddMemberAction {
|
||||||
|
added?: MemberClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteMemberAction {
|
||||||
|
deletedUserId?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyMemberRoleAction {
|
||||||
|
userId?: ProtoBinaryType;
|
||||||
|
role?: MemberRoleEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyMemberProfileKeyAction {
|
||||||
|
presentation?: ProtoBinaryType;
|
||||||
|
|
||||||
|
// The result of decryption
|
||||||
|
profileKey: ArrayBuffer;
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddPendingMemberAction {
|
||||||
|
added?: PendingMemberClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeletePendingMemberAction {
|
||||||
|
deletedUserId?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PromotePendingMemberAction {
|
||||||
|
presentation?: ProtoBinaryType;
|
||||||
|
|
||||||
|
// The result of decryption
|
||||||
|
profileKey: ArrayBuffer;
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyTitleAction {
|
||||||
|
title?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyAvatarAction {
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyDisappearingMessagesTimerAction {
|
||||||
|
timer?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyAttributesAccessControlAction {
|
||||||
|
attributesAccess?: AccessRequiredEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifyMembersAccessControlAction {
|
||||||
|
membersAccess?: AccessRequiredEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class GroupChangesClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => GroupChangesClass;
|
||||||
|
|
||||||
|
groupChanges?: Array<GroupChangesClass.GroupChangeState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace GroupChangesClass {
|
||||||
|
class GroupChangeState {
|
||||||
|
groupChange?: GroupChangeClass;
|
||||||
|
groupState?: GroupClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class GroupAttributeBlobClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => GroupAttributeBlobClass;
|
||||||
|
toArrayBuffer(): ArrayBuffer;
|
||||||
|
|
||||||
|
title?: string;
|
||||||
|
avatar?: ProtoBinaryType;
|
||||||
|
disappearingMessagesDuration?: number;
|
||||||
|
|
||||||
|
// Note: this isn't part of the proto, but our protobuf library tells us which
|
||||||
|
// field has been set with this prop.
|
||||||
|
content: 'title' | 'avatar' | 'disappearingMessagesDuration';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous protos
|
||||||
|
|
||||||
export declare class AttachmentPointerClass {
|
export declare class AttachmentPointerClass {
|
||||||
static decode: (
|
static decode: (
|
||||||
data: ArrayBuffer | ByteBufferClass,
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
@ -435,6 +686,11 @@ export declare class GroupContextV2Class {
|
||||||
masterKey?: ProtoBinaryType;
|
masterKey?: ProtoBinaryType;
|
||||||
revision?: number;
|
revision?: number;
|
||||||
groupChange?: ProtoBinaryType;
|
groupChange?: ProtoBinaryType;
|
||||||
|
|
||||||
|
// Note: these additional properties are added in the course of processing
|
||||||
|
id?: string;
|
||||||
|
secretParams?: string;
|
||||||
|
publicParams?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: we need to use namespaces to express nested classes in Typescript
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
@ -674,7 +930,7 @@ export declare class GroupV2RecordClass {
|
||||||
) => GroupV2RecordClass;
|
) => GroupV2RecordClass;
|
||||||
toArrayBuffer: () => ArrayBuffer;
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
masterKey?: ByteBufferClass | null;
|
masterKey?: ProtoBinaryType | null;
|
||||||
blocked?: boolean | null;
|
blocked?: boolean | null;
|
||||||
whitelisted?: boolean | null;
|
whitelisted?: boolean | null;
|
||||||
archived?: boolean | null;
|
archived?: boolean | null;
|
||||||
|
@ -754,6 +1010,7 @@ export declare namespace SyncMessageClass {
|
||||||
unidentifiedDeliveryIndicators?: boolean;
|
unidentifiedDeliveryIndicators?: boolean;
|
||||||
typingIndicators?: boolean;
|
typingIndicators?: boolean;
|
||||||
linkPreviews?: boolean;
|
linkPreviews?: boolean;
|
||||||
|
provisioningVersion?: number;
|
||||||
}
|
}
|
||||||
class Contacts {
|
class Contacts {
|
||||||
blob?: AttachmentPointerClass;
|
blob?: AttachmentPointerClass;
|
||||||
|
|
|
@ -610,6 +610,11 @@ export default class AccountManager extends EventTarget {
|
||||||
store.clearSessionStore(),
|
store.clearSessionStore(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getGroupCredentials(startDay: number, endDay: number) {
|
||||||
|
return this.server.getGroupCredentials(startDay, endDay);
|
||||||
|
}
|
||||||
|
|
||||||
// Takes the same object returned by generateKeys
|
// Takes the same object returned by generateKeys
|
||||||
async confirmKeys(keys: GeneratedKeysType) {
|
async confirmKeys(keys: GeneratedKeysType) {
|
||||||
const store = window.textsecure.storage.protocol;
|
const store = window.textsecure.storage.protocol;
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
VerifiedClass,
|
VerifiedClass,
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
|
|
||||||
|
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
||||||
|
|
||||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -435,6 +437,9 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
hasEmptied(): boolean {
|
||||||
|
return Boolean(this.isEmptied);
|
||||||
|
}
|
||||||
onEmpty() {
|
onEmpty() {
|
||||||
const emitEmpty = () => {
|
const emitEmpty = () => {
|
||||||
window.log.info("MessageReceiver: emitting 'empty' event");
|
window.log.info("MessageReceiver: emitting 'empty' event");
|
||||||
|
@ -1070,14 +1075,6 @@ class MessageReceiverInner extends EventTarget {
|
||||||
throw new Error('MessageReceiver.handleSentMessage: message was falsey!');
|
throw new Error('MessageReceiver.handleSentMessage: message was falsey!');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.groupV2) {
|
|
||||||
window.log.warn(
|
|
||||||
'MessageReceiver.handleSentMessage: Dropping GroupsV2 message'
|
|
||||||
);
|
|
||||||
this.removeFromCache(envelope);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let p: Promise<any> = Promise.resolve();
|
let p: Promise<any> = Promise.resolve();
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
if (
|
if (
|
||||||
|
@ -1094,7 +1091,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
return p.then(async () =>
|
return p.then(async () =>
|
||||||
this.processDecrypted(envelope, msg).then(message => {
|
this.processDecrypted(envelope, msg).then(message => {
|
||||||
const groupId = message.group && message.group.id;
|
// prettier-ignore
|
||||||
|
const groupId = this.getGroupId(message);
|
||||||
const isBlocked = this.isGroupBlocked(groupId);
|
const isBlocked = this.isGroupBlocked(groupId);
|
||||||
const { source, sourceUuid } = envelope;
|
const { source, sourceUuid } = envelope;
|
||||||
const ourE164 = window.textsecure.storage.user.getNumber();
|
const ourE164 = window.textsecure.storage.user.getNumber();
|
||||||
|
@ -1103,7 +1101,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
(source && ourE164 && source === ourE164) ||
|
(source && ourE164 && source === ourE164) ||
|
||||||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
|
(sourceUuid && ourUuid && sourceUuid === ourUuid);
|
||||||
const isLeavingGroup = Boolean(
|
const isLeavingGroup = Boolean(
|
||||||
message.group &&
|
!message.groupV2 &&
|
||||||
|
message.group &&
|
||||||
message.group.type ===
|
message.group.type ===
|
||||||
window.textsecure.protobuf.GroupContext.Type.QUIT
|
window.textsecure.protobuf.GroupContext.Type.QUIT
|
||||||
);
|
);
|
||||||
|
@ -1148,14 +1147,16 @@ class MessageReceiverInner extends EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.groupV2) {
|
if (!window.GV2 && msg.groupV2) {
|
||||||
window.log.warn(
|
|
||||||
'MessageReceiver.handleDataMessage: Dropping GroupsV2 message'
|
|
||||||
);
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
|
window.log.info(
|
||||||
|
'MessageReceiver.handleDataMessage: dropping GroupV2 message'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.deriveGroupsV2Data(msg);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
msg.flags &&
|
msg.flags &&
|
||||||
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
|
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
|
||||||
|
@ -1180,7 +1181,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
return p.then(async () =>
|
return p.then(async () =>
|
||||||
this.processDecrypted(envelope, msg).then(message => {
|
this.processDecrypted(envelope, msg).then(message => {
|
||||||
const groupId = message.group && message.group.id;
|
// prettier-ignore
|
||||||
|
const groupId = this.getGroupId(message);
|
||||||
const isBlocked = this.isGroupBlocked(groupId);
|
const isBlocked = this.isGroupBlocked(groupId);
|
||||||
const { source, sourceUuid } = envelope;
|
const { source, sourceUuid } = envelope;
|
||||||
const ourE164 = window.textsecure.storage.user.getNumber();
|
const ourE164 = window.textsecure.storage.user.getNumber();
|
||||||
|
@ -1189,7 +1191,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
(source && ourE164 && source === ourE164) ||
|
(source && ourE164 && source === ourE164) ||
|
||||||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
|
(sourceUuid && ourUuid && sourceUuid === ourUuid);
|
||||||
const isLeavingGroup = Boolean(
|
const isLeavingGroup = Boolean(
|
||||||
message.group &&
|
!message.groupV2 &&
|
||||||
|
message.group &&
|
||||||
message.group.type ===
|
message.group.type ===
|
||||||
window.textsecure.protobuf.GroupContext.Type.QUIT
|
window.textsecure.protobuf.GroupContext.Type.QUIT
|
||||||
);
|
);
|
||||||
|
@ -1336,23 +1339,26 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { groupId, timestamp, action } = typingMessage;
|
||||||
|
|
||||||
ev.sender = envelope.source;
|
ev.sender = envelope.source;
|
||||||
ev.senderUuid = envelope.sourceUuid;
|
ev.senderUuid = envelope.sourceUuid;
|
||||||
ev.senderDevice = envelope.sourceDevice;
|
ev.senderDevice = envelope.sourceDevice;
|
||||||
ev.typing = {
|
ev.typing = {
|
||||||
typingMessage,
|
typingMessage,
|
||||||
timestamp: typingMessage.timestamp
|
timestamp: timestamp ? timestamp.toNumber() : Date.now(),
|
||||||
? typingMessage.timestamp.toNumber()
|
groupId:
|
||||||
: Date.now(),
|
groupId && groupId.buffer.byteLength < 45
|
||||||
groupId: typingMessage.groupId
|
? groupId.toString('binary')
|
||||||
? typingMessage.groupId.toString('binary')
|
: null,
|
||||||
: null,
|
groupV2Id:
|
||||||
|
groupId && groupId.buffer.byteLength >= 45
|
||||||
|
? groupId.toString('base64')
|
||||||
|
: null,
|
||||||
started:
|
started:
|
||||||
typingMessage.action ===
|
action === window.textsecure.protobuf.TypingMessage.Action.STARTED,
|
||||||
window.textsecure.protobuf.TypingMessage.Action.STARTED,
|
|
||||||
stopped:
|
stopped:
|
||||||
typingMessage.action ===
|
action === window.textsecure.protobuf.TypingMessage.Action.STOPPED,
|
||||||
window.textsecure.protobuf.TypingMessage.Action.STOPPED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.dispatchEvent(ev);
|
return this.dispatchEvent(ev);
|
||||||
|
@ -1362,6 +1368,60 @@ class MessageReceiverInner extends EventTarget {
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deriveGroupsV2Data(message: DataMessageClass) {
|
||||||
|
const { groupV2 } = message;
|
||||||
|
|
||||||
|
if (!groupV2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNumber(groupV2.revision)) {
|
||||||
|
throw new Error('deriveGroupsV2Data: revision was not a number');
|
||||||
|
}
|
||||||
|
if (!groupV2.masterKey) {
|
||||||
|
throw new Error('deriveGroupsV2Data: had falsey masterKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64;
|
||||||
|
const masterKey: ArrayBuffer = groupV2.masterKey.toArrayBuffer();
|
||||||
|
const length = masterKey.byteLength;
|
||||||
|
if (length !== MASTER_KEY_LENGTH) {
|
||||||
|
throw new Error(
|
||||||
|
`deriveGroupsV2Data: masterKey had length ${length}, expected ${MASTER_KEY_LENGTH}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = deriveGroupFields(masterKey);
|
||||||
|
groupV2.masterKey = toBase64(masterKey);
|
||||||
|
groupV2.secretParams = toBase64(fields.secretParams);
|
||||||
|
groupV2.publicParams = toBase64(fields.publicParams);
|
||||||
|
groupV2.id = toBase64(fields.id);
|
||||||
|
|
||||||
|
if (groupV2.groupChange) {
|
||||||
|
groupV2.groupChange = groupV2.groupChange.toString('base64');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getGroupId(message: DataMessageClass) {
|
||||||
|
if (message.groupV2) {
|
||||||
|
return message.groupV2.id;
|
||||||
|
}
|
||||||
|
if (message.group) {
|
||||||
|
return message.group.id.toString('binary');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDestination(sentMessage: SyncMessageClass.Sent) {
|
||||||
|
if (sentMessage.message && sentMessage.message.groupV2) {
|
||||||
|
return `groupv2(${sentMessage.message.groupV2.id})`;
|
||||||
|
} else if (sentMessage.message && sentMessage.message.group) {
|
||||||
|
return `group(${sentMessage.message.group.id.toBinary()})`;
|
||||||
|
} else {
|
||||||
|
return sentMessage.destination || sentMessage.destinationUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity
|
// tslint:disable-next-line cyclomatic-complexity
|
||||||
async handleSyncMessage(
|
async handleSyncMessage(
|
||||||
envelope: EnvelopeClass,
|
envelope: EnvelopeClass,
|
||||||
|
@ -1399,13 +1459,20 @@ class MessageReceiverInner extends EventTarget {
|
||||||
'MessageReceiver.handleSyncMessage: sync sent message was missing message'
|
'MessageReceiver.handleSyncMessage: sync sent message was missing message'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const to = sentMessage.message.group
|
|
||||||
? `group(${sentMessage.message.group.id.toBinary()})`
|
if (!window.GV2 && sentMessage.message.groupV2) {
|
||||||
: sentMessage.destination || sentMessage.destinationUuid;
|
this.removeFromCache(envelope);
|
||||||
|
window.log.info(
|
||||||
|
'MessageReceiver.handleSyncMessage: dropping GroupV2 message'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deriveGroupsV2Data(sentMessage.message);
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'sent message to',
|
'sent message to',
|
||||||
to,
|
this.getDestination(sentMessage),
|
||||||
sentMessage.timestamp.toNumber(),
|
sentMessage.timestamp.toNumber(),
|
||||||
'from',
|
'from',
|
||||||
this.getEnvelopeId(envelope)
|
this.getEnvelopeId(envelope)
|
||||||
|
@ -1939,6 +2006,13 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { reaction } = decrypted;
|
||||||
|
if (reaction) {
|
||||||
|
if (reaction.targetTimestamp) {
|
||||||
|
reaction.targetTimestamp = reaction.targetTimestamp.toNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(decrypted);
|
return Promise.resolve(decrypted);
|
||||||
/* eslint-enable no-bitwise, no-param-reassign */
|
/* eslint-enable no-bitwise, no-param-reassign */
|
||||||
}
|
}
|
||||||
|
@ -1964,11 +2038,11 @@ export default class MessageReceiver {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addEventListener = inner.addEventListener.bind(inner);
|
this.addEventListener = inner.addEventListener.bind(inner);
|
||||||
this.removeEventListener = inner.removeEventListener.bind(inner);
|
|
||||||
this.getStatus = inner.getStatus.bind(inner);
|
|
||||||
this.close = inner.close.bind(inner);
|
this.close = inner.close.bind(inner);
|
||||||
|
|
||||||
this.downloadAttachment = inner.downloadAttachment.bind(inner);
|
this.downloadAttachment = inner.downloadAttachment.bind(inner);
|
||||||
|
this.getStatus = inner.getStatus.bind(inner);
|
||||||
|
this.hasEmptied = inner.hasEmptied.bind(inner);
|
||||||
|
this.removeEventListener = inner.removeEventListener.bind(inner);
|
||||||
this.stopProcessing = inner.stopProcessing.bind(inner);
|
this.stopProcessing = inner.stopProcessing.bind(inner);
|
||||||
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
|
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
|
||||||
|
|
||||||
|
@ -1976,12 +2050,13 @@ export default class MessageReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener: (name: string, handler: Function) => void;
|
addEventListener: (name: string, handler: Function) => void;
|
||||||
removeEventListener: (name: string, handler: Function) => void;
|
|
||||||
getStatus: () => number;
|
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
downloadAttachment: (
|
downloadAttachment: (
|
||||||
attachment: AttachmentPointerClass
|
attachment: AttachmentPointerClass
|
||||||
) => Promise<DownloadAttachmentType>;
|
) => Promise<DownloadAttachmentType>;
|
||||||
|
getStatus: () => number;
|
||||||
|
hasEmptied: () => boolean;
|
||||||
|
removeEventListener: (name: string, handler: Function) => void;
|
||||||
stopProcessing: () => Promise<void>;
|
stopProcessing: () => Promise<void>;
|
||||||
unregisterBatchers: () => void;
|
unregisterBatchers: () => void;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { reject } from 'lodash';
|
import { reject } from 'lodash';
|
||||||
import { ServerKeysType, WebAPIType } from './WebAPI';
|
import { ServerKeysType, WebAPIType } from './WebAPI';
|
||||||
|
import { isEnabled as isRemoteFlagEnabled } from '../RemoteConfig';
|
||||||
import { SignalProtocolAddressClass } from '../libsignal.d';
|
import { SignalProtocolAddressClass } from '../libsignal.d';
|
||||||
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
||||||
import {
|
import {
|
||||||
|
@ -574,34 +575,36 @@ export default class OutgoingMessage {
|
||||||
async sendToIdentifier(providedIdentifier: string) {
|
async sendToIdentifier(providedIdentifier: string) {
|
||||||
let identifier = providedIdentifier;
|
let identifier = providedIdentifier;
|
||||||
try {
|
try {
|
||||||
if (window.isValidGuid(identifier)) {
|
if (isRemoteFlagEnabled('desktop.cds')) {
|
||||||
// We're good!
|
if (window.isValidGuid(identifier)) {
|
||||||
} else if (isValidNumber(identifier)) {
|
// We're good!
|
||||||
if (!window.textsecure.messaging) {
|
} else if (isValidNumber(identifier)) {
|
||||||
throw new Error(
|
if (!window.textsecure.messaging) {
|
||||||
'sendToIdentifier: window.textsecure.messaging is not available!'
|
throw new Error(
|
||||||
);
|
'sendToIdentifier: window.textsecure.messaging is not available!'
|
||||||
}
|
);
|
||||||
const lookup = await window.textsecure.messaging.getUuidsForE164s([
|
}
|
||||||
identifier,
|
const lookup = await window.textsecure.messaging.getUuidsForE164s([
|
||||||
]);
|
|
||||||
const uuid = lookup[identifier];
|
|
||||||
if (uuid) {
|
|
||||||
this.discoveredIdentifierPairs.push({
|
|
||||||
uuid,
|
|
||||||
e164: identifier,
|
|
||||||
});
|
|
||||||
identifier = uuid;
|
|
||||||
} else {
|
|
||||||
throw new UnregisteredUserError(
|
|
||||||
identifier,
|
identifier,
|
||||||
new Error('User is not registered')
|
]);
|
||||||
|
const uuid = lookup[identifier];
|
||||||
|
if (uuid) {
|
||||||
|
this.discoveredIdentifierPairs.push({
|
||||||
|
uuid,
|
||||||
|
e164: identifier,
|
||||||
|
});
|
||||||
|
identifier = uuid;
|
||||||
|
} else {
|
||||||
|
throw new UnregisteredUserError(
|
||||||
|
identifier,
|
||||||
|
new Error('User is not registered')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`sendToIdentifier: identifier ${identifier} was neither a UUID or E164`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`sendToIdentifier: identifier ${identifier} was neither a UUID or E164`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
||||||
|
|
|
@ -1,17 +1,31 @@
|
||||||
// tslint:disable no-bitwise no-default-export
|
// tslint:disable no-bitwise no-default-export
|
||||||
|
|
||||||
import { without } from 'lodash';
|
import { Dictionary, without } from 'lodash';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
|
||||||
import { ProxiedRequestOptionsType, WebAPIType } from './WebAPI';
|
import {
|
||||||
|
GroupCredentialsType,
|
||||||
|
GroupLogResponseType,
|
||||||
|
ProxiedRequestOptionsType,
|
||||||
|
WebAPIType,
|
||||||
|
} from './WebAPI';
|
||||||
import createTaskWithTimeout from './TaskWithTimeout';
|
import createTaskWithTimeout from './TaskWithTimeout';
|
||||||
import OutgoingMessage from './OutgoingMessage';
|
import OutgoingMessage from './OutgoingMessage';
|
||||||
import Crypto from './Crypto';
|
import Crypto from './Crypto';
|
||||||
|
import {
|
||||||
|
base64ToArrayBuffer,
|
||||||
|
concatenateBytes,
|
||||||
|
fromEncodedBinaryToArrayBuffer,
|
||||||
|
getZeroes,
|
||||||
|
hexToArrayBuffer,
|
||||||
|
} from '../Crypto';
|
||||||
import {
|
import {
|
||||||
AttachmentPointerClass,
|
AttachmentPointerClass,
|
||||||
CallingMessageClass,
|
CallingMessageClass,
|
||||||
ContentClass,
|
ContentClass,
|
||||||
DataMessageClass,
|
DataMessageClass,
|
||||||
|
GroupChangeClass,
|
||||||
|
GroupClass,
|
||||||
StorageServiceCallOptionsType,
|
StorageServiceCallOptionsType,
|
||||||
StorageServiceCredentials,
|
StorageServiceCredentials,
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
|
@ -28,12 +42,6 @@ function stringToArrayBuffer(str: string): ArrayBuffer {
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
function hexStringToArrayBuffer(string: string): ArrayBuffer {
|
|
||||||
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
|
||||||
}
|
|
||||||
function base64ToArrayBuffer(string: string): ArrayBuffer {
|
|
||||||
return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendMetadataType = {
|
export type SendMetadataType = {
|
||||||
[identifier: string]: {
|
[identifier: string]: {
|
||||||
|
@ -70,6 +78,17 @@ type QuoteAttachmentType = {
|
||||||
attachmentPointer?: AttachmentPointerClass;
|
attachmentPointer?: AttachmentPointerClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GroupV2InfoType = {
|
||||||
|
groupChange?: ArrayBuffer;
|
||||||
|
masterKey: ArrayBuffer;
|
||||||
|
revision: number;
|
||||||
|
members: Array<string>;
|
||||||
|
};
|
||||||
|
type GroupV1InfoType = {
|
||||||
|
id: string;
|
||||||
|
members: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
type MessageOptionsType = {
|
type MessageOptionsType = {
|
||||||
attachments?: Array<AttachmentType> | null;
|
attachments?: Array<AttachmentType> | null;
|
||||||
body?: string;
|
body?: string;
|
||||||
|
@ -79,6 +98,7 @@ type MessageOptionsType = {
|
||||||
id: string;
|
id: string;
|
||||||
type: number;
|
type: number;
|
||||||
};
|
};
|
||||||
|
groupV2?: GroupV2InfoType;
|
||||||
needsSync?: boolean;
|
needsSync?: boolean;
|
||||||
preview?: Array<PreviewType> | null;
|
preview?: Array<PreviewType> | null;
|
||||||
profileKey?: ArrayBuffer;
|
profileKey?: ArrayBuffer;
|
||||||
|
@ -98,6 +118,7 @@ class Message {
|
||||||
id: string;
|
id: string;
|
||||||
type: number;
|
type: number;
|
||||||
};
|
};
|
||||||
|
groupV2?: GroupV2InfoType;
|
||||||
needsSync?: boolean;
|
needsSync?: boolean;
|
||||||
preview: any;
|
preview: any;
|
||||||
profileKey?: ArrayBuffer;
|
profileKey?: ArrayBuffer;
|
||||||
|
@ -117,6 +138,7 @@ class Message {
|
||||||
this.expireTimer = options.expireTimer;
|
this.expireTimer = options.expireTimer;
|
||||||
this.flags = options.flags;
|
this.flags = options.flags;
|
||||||
this.group = options.group;
|
this.group = options.group;
|
||||||
|
this.groupV2 = options.groupV2;
|
||||||
this.needsSync = options.needsSync;
|
this.needsSync = options.needsSync;
|
||||||
this.preview = options.preview;
|
this.preview = options.preview;
|
||||||
this.profileKey = options.profileKey;
|
this.profileKey = options.profileKey;
|
||||||
|
@ -130,7 +152,7 @@ class Message {
|
||||||
throw new Error('Invalid recipient list');
|
throw new Error('Invalid recipient list');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.group && this.recipients.length !== 1) {
|
if (!this.group && !this.groupV2 && this.recipients.length !== 1) {
|
||||||
throw new Error('Invalid recipient list for non-group');
|
throw new Error('Invalid recipient list for non-group');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,14 +224,19 @@ class Message {
|
||||||
if (this.flags) {
|
if (this.flags) {
|
||||||
proto.flags = this.flags;
|
proto.flags = this.flags;
|
||||||
}
|
}
|
||||||
if (this.group) {
|
if (this.groupV2) {
|
||||||
|
proto.groupV2 = new window.textsecure.protobuf.GroupContextV2();
|
||||||
|
proto.groupV2.masterKey = this.groupV2.masterKey;
|
||||||
|
proto.groupV2.revision = this.groupV2.revision;
|
||||||
|
proto.groupV2.groupChange = this.groupV2.groupChange || null;
|
||||||
|
} else if (this.group) {
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
proto.group = new window.textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(this.group.id);
|
proto.group.id = stringToArrayBuffer(this.group.id);
|
||||||
proto.group.type = this.group.type;
|
proto.group.type = this.group.type;
|
||||||
}
|
}
|
||||||
if (this.sticker) {
|
if (this.sticker) {
|
||||||
proto.sticker = new window.textsecure.protobuf.DataMessage.Sticker();
|
proto.sticker = new window.textsecure.protobuf.DataMessage.Sticker();
|
||||||
proto.sticker.packId = hexStringToArrayBuffer(this.sticker.packId);
|
proto.sticker.packId = hexToArrayBuffer(this.sticker.packId);
|
||||||
proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey);
|
proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey);
|
||||||
proto.sticker.stickerId = this.sticker.stickerId;
|
proto.sticker.stickerId = this.sticker.stickerId;
|
||||||
|
|
||||||
|
@ -306,9 +333,9 @@ export default class MessageSender {
|
||||||
getPaddedAttachment(data: ArrayBuffer) {
|
getPaddedAttachment(data: ArrayBuffer) {
|
||||||
const size = data.byteLength;
|
const size = data.byteLength;
|
||||||
const paddedSize = this._getAttachmentSizeBucket(size);
|
const paddedSize = this._getAttachmentSizeBucket(size);
|
||||||
const padding = window.Signal.Crypto.getZeroes(paddedSize - size);
|
const padding = getZeroes(paddedSize - size);
|
||||||
|
|
||||||
return window.Signal.Crypto.concatenateBytes(data, padding);
|
return concatenateBytes(data, padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeAttachmentPointer(attachment: AttachmentType) {
|
async makeAttachmentPointer(attachment: AttachmentType) {
|
||||||
|
@ -704,7 +731,9 @@ export default class MessageSender {
|
||||||
return this.server.getProfile(number, options);
|
return this.server.getProfile(number, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUuidsForE164s(numbers: Array<string>) {
|
async getUuidsForE164s(
|
||||||
|
numbers: Array<string>
|
||||||
|
): Promise<Dictionary<string | null>> {
|
||||||
return this.server.getUuidsForE164s(numbers);
|
return this.server.getUuidsForE164s(numbers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -882,14 +911,14 @@ export default class MessageSender {
|
||||||
options: {
|
options: {
|
||||||
recipientId: string;
|
recipientId: string;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
groupNumbers: Array<string>;
|
groupMembers: Array<string>;
|
||||||
isTyping: boolean;
|
isTyping: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
},
|
},
|
||||||
sendOptions: SendOptionsType = {}
|
sendOptions: SendOptionsType = {}
|
||||||
) {
|
) {
|
||||||
const ACTION_ENUM = window.textsecure.protobuf.TypingMessage.Action;
|
const ACTION_ENUM = window.textsecure.protobuf.TypingMessage.Action;
|
||||||
const { recipientId, groupId, groupNumbers, isTyping, timestamp } = options;
|
const { recipientId, groupId, groupMembers, isTyping, timestamp } = options;
|
||||||
|
|
||||||
// We don't want to send typing messages to our other devices, but we will
|
// We don't want to send typing messages to our other devices, but we will
|
||||||
// in the group case.
|
// in the group case.
|
||||||
|
@ -904,10 +933,10 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipients = groupId
|
const recipients = groupId
|
||||||
? (without(groupNumbers, myNumber, myUuid) as Array<string>)
|
? (without(groupMembers, myNumber, myUuid) as Array<string>)
|
||||||
: [recipientId];
|
: [recipientId];
|
||||||
const groupIdBuffer = groupId
|
const groupIdBuffer = groupId
|
||||||
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
|
? fromEncodedBinaryToArrayBuffer(groupId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
|
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
|
||||||
|
@ -1175,7 +1204,7 @@ export default class MessageSender {
|
||||||
const { packId, packKey, installed } = item;
|
const { packId, packKey, installed } = item;
|
||||||
|
|
||||||
const operation = new window.textsecure.protobuf.SyncMessage.StickerPackOperation();
|
const operation = new window.textsecure.protobuf.SyncMessage.StickerPackOperation();
|
||||||
operation.packId = hexStringToArrayBuffer(packId);
|
operation.packId = hexToArrayBuffer(packId);
|
||||||
operation.packKey = base64ToArrayBuffer(packKey);
|
operation.packKey = base64ToArrayBuffer(packKey);
|
||||||
operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE;
|
operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE;
|
||||||
|
|
||||||
|
@ -1466,21 +1495,48 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessageToGroup(
|
async sendMessageToGroup(
|
||||||
groupId: string,
|
{
|
||||||
recipients: Array<string>,
|
attachments,
|
||||||
messageText: string,
|
expireTimer,
|
||||||
attachments: Array<AttachmentType>,
|
groupV2,
|
||||||
quote: any,
|
groupV1,
|
||||||
preview: any,
|
messageText,
|
||||||
sticker: any,
|
preview,
|
||||||
reaction: any,
|
profileKey,
|
||||||
timestamp: number,
|
quote,
|
||||||
expireTimer: number | undefined,
|
reaction,
|
||||||
profileKey?: ArrayBuffer,
|
sticker,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
attachments?: Array<AttachmentType>;
|
||||||
|
expireTimer?: number;
|
||||||
|
groupV2?: GroupV2InfoType;
|
||||||
|
groupV1?: GroupV1InfoType;
|
||||||
|
messageText?: string;
|
||||||
|
preview?: any;
|
||||||
|
profileKey?: ArrayBuffer;
|
||||||
|
quote?: any;
|
||||||
|
reaction?: any;
|
||||||
|
sticker?: any;
|
||||||
|
timestamp: number;
|
||||||
|
},
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
): Promise<CallbackResultType> {
|
): Promise<CallbackResultType> {
|
||||||
|
if (!groupV1 && !groupV2) {
|
||||||
|
throw new Error(
|
||||||
|
'sendMessageToGroup: Neither group1 nor groupv2 information provided!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const myE164 = window.textsecure.storage.user.getNumber();
|
const myE164 = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getNumber();
|
const myUuid = window.textsecure.storage.user.getNumber();
|
||||||
|
// prettier-ignore
|
||||||
|
const recipients = groupV2
|
||||||
|
? groupV2.members
|
||||||
|
: groupV1
|
||||||
|
? groupV1.members
|
||||||
|
: [];
|
||||||
|
|
||||||
const attrs = {
|
const attrs = {
|
||||||
recipients: recipients.filter(r => r !== myE164 && r !== myUuid),
|
recipients: recipients.filter(r => r !== myE164 && r !== myUuid),
|
||||||
body: messageText,
|
body: messageText,
|
||||||
|
@ -1492,10 +1548,13 @@ export default class MessageSender {
|
||||||
reaction,
|
reaction,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
group: {
|
groupV2,
|
||||||
id: groupId,
|
group: groupV1
|
||||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
? {
|
||||||
},
|
id: groupV1.id,
|
||||||
|
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (recipients.length === 0) {
|
if (recipients.length === 0) {
|
||||||
|
@ -1512,138 +1571,25 @@ export default class MessageSender {
|
||||||
return this.sendMessage(attrs, options);
|
return this.sendMessage(attrs, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createGroup(
|
async getGroup(options: GroupCredentialsType): Promise<GroupClass> {
|
||||||
targetIdentifiers: Array<string>,
|
return this.server.getGroup(options);
|
||||||
id: string,
|
}
|
||||||
name: string,
|
async getGroupLog(
|
||||||
avatar: AttachmentType,
|
startVersion: number,
|
||||||
options?: SendOptionsType
|
options: GroupCredentialsType
|
||||||
) {
|
): Promise<GroupLogResponseType> {
|
||||||
const proto = new window.textsecure.protobuf.DataMessage();
|
return this.server.getGroupLog(startVersion, options);
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
}
|
||||||
proto.group.id = stringToArrayBuffer(id);
|
async getGroupAvatar(key: string): Promise<ArrayBuffer> {
|
||||||
|
return this.server.getGroupAvatar(key);
|
||||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.UPDATE;
|
}
|
||||||
proto.group.membersE164 = targetIdentifiers;
|
async modifyGroup(
|
||||||
proto.group.name = name;
|
changes: GroupChangeClass.Actions,
|
||||||
|
options: GroupCredentialsType
|
||||||
return this.makeAttachmentPointer(avatar).then(async attachment => {
|
): Promise<GroupChangeClass> {
|
||||||
if (!proto.group) {
|
return this.server.modifyGroup(changes, options);
|
||||||
throw new Error('createGroup: proto.group was set to null');
|
|
||||||
}
|
|
||||||
proto.group.avatar = attachment;
|
|
||||||
return this.sendGroupProto(
|
|
||||||
targetIdentifiers,
|
|
||||||
proto,
|
|
||||||
Date.now(),
|
|
||||||
options
|
|
||||||
).then(() => {
|
|
||||||
if (!proto.group) {
|
|
||||||
throw new Error('createGroup: proto.group was set to null');
|
|
||||||
}
|
|
||||||
|
|
||||||
return proto.group.id;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateGroup(
|
|
||||||
groupId: string,
|
|
||||||
name: string,
|
|
||||||
avatar: AttachmentType,
|
|
||||||
targetIdentifiers: Array<string>,
|
|
||||||
options?: SendOptionsType
|
|
||||||
) {
|
|
||||||
const proto = new window.textsecure.protobuf.DataMessage();
|
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
|
||||||
|
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
|
||||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.UPDATE;
|
|
||||||
proto.group.name = name;
|
|
||||||
proto.group.membersE164 = targetIdentifiers;
|
|
||||||
|
|
||||||
return this.makeAttachmentPointer(avatar).then(async attachment => {
|
|
||||||
if (!proto.group) {
|
|
||||||
throw new Error('updateGroup: proto.group was set to null');
|
|
||||||
}
|
|
||||||
|
|
||||||
proto.group.avatar = attachment;
|
|
||||||
return this.sendGroupProto(
|
|
||||||
targetIdentifiers,
|
|
||||||
proto,
|
|
||||||
Date.now(),
|
|
||||||
options
|
|
||||||
).then(() => {
|
|
||||||
if (!proto.group) {
|
|
||||||
throw new Error('updateGroup: proto.group was set to null');
|
|
||||||
}
|
|
||||||
return proto.group.id;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async addIdentifierToGroup(
|
|
||||||
groupId: string,
|
|
||||||
newIdentifiers: Array<string>,
|
|
||||||
options: SendOptionsType
|
|
||||||
) {
|
|
||||||
const proto = new window.textsecure.protobuf.DataMessage();
|
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
|
||||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.UPDATE;
|
|
||||||
proto.group.membersE164 = newIdentifiers;
|
|
||||||
return this.sendGroupProto(newIdentifiers, proto, Date.now(), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setGroupName(
|
|
||||||
groupId: string,
|
|
||||||
name: string,
|
|
||||||
groupIdentifiers: Array<string>,
|
|
||||||
options: SendOptionsType
|
|
||||||
) {
|
|
||||||
const proto = new window.textsecure.protobuf.DataMessage();
|
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
|
||||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.UPDATE;
|
|
||||||
proto.group.name = name;
|
|
||||||
proto.group.membersE164 = groupIdentifiers;
|
|
||||||
|
|
||||||
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setGroupAvatar(
|
|
||||||
groupId: string,
|
|
||||||
avatar: AttachmentType,
|
|
||||||
groupIdentifiers: Array<string>,
|
|
||||||
options: SendOptionsType
|
|
||||||
) {
|
|
||||||
const proto = new window.textsecure.protobuf.DataMessage();
|
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
|
||||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.UPDATE;
|
|
||||||
proto.group.membersE164 = groupIdentifiers;
|
|
||||||
|
|
||||||
return this.makeAttachmentPointer(avatar).then(async attachment => {
|
|
||||||
if (!proto.group) {
|
|
||||||
throw new Error('setGroupAvatar: proto.group was set to null');
|
|
||||||
}
|
|
||||||
|
|
||||||
proto.group.avatar = attachment;
|
|
||||||
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async leaveGroup(
|
|
||||||
groupId: string,
|
|
||||||
groupIdentifiers: Array<string>,
|
|
||||||
options?: SendOptionsType
|
|
||||||
) {
|
|
||||||
const proto = new window.textsecure.protobuf.DataMessage();
|
|
||||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
|
||||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.QUIT;
|
|
||||||
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
|
|
||||||
}
|
|
||||||
async sendExpirationTimerUpdateToGroup(
|
async sendExpirationTimerUpdateToGroup(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
groupIdentifiers: Array<string>,
|
groupIdentifiers: Array<string>,
|
||||||
|
@ -1683,6 +1629,7 @@ export default class MessageSender {
|
||||||
|
|
||||||
return this.sendMessage(attrs, options);
|
return this.sendMessage(attrs, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendExpirationTimerUpdateToIdentifier(
|
async sendExpirationTimerUpdateToIdentifier(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
expireTimer: number | undefined,
|
expireTimer: number | undefined,
|
||||||
|
|
|
@ -35,6 +35,10 @@ import PQueue from 'p-queue';
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AvatarUploadAttributesClass,
|
||||||
|
GroupChangeClass,
|
||||||
|
GroupChangesClass,
|
||||||
|
GroupClass,
|
||||||
StorageServiceCallOptionsType,
|
StorageServiceCallOptionsType,
|
||||||
StorageServiceCredentials,
|
StorageServiceCredentials,
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
|
@ -283,6 +287,7 @@ type RedactUrl = (url: string) => string;
|
||||||
|
|
||||||
type PromiseAjaxOptionsType = {
|
type PromiseAjaxOptionsType = {
|
||||||
accessKey?: string;
|
accessKey?: string;
|
||||||
|
basicAuth?: string;
|
||||||
certificateAuthority?: string;
|
certificateAuthority?: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
data?: ArrayBuffer | Buffer | string;
|
data?: ArrayBuffer | Buffer | string;
|
||||||
|
@ -309,7 +314,12 @@ type PromiseAjaxOptionsType = {
|
||||||
|
|
||||||
type JSONWithDetailsType = {
|
type JSONWithDetailsType = {
|
||||||
data: any;
|
data: any;
|
||||||
contentType: string;
|
contentType: string | null;
|
||||||
|
response: Response;
|
||||||
|
};
|
||||||
|
type ArrayBufferWithDetailsType = {
|
||||||
|
data: ArrayBuffer;
|
||||||
|
contentType: string | null;
|
||||||
response: Response;
|
response: Response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -377,8 +387,10 @@ async function _promiseAjax(
|
||||||
fetchOptions.headers['Content-Length'] = contentLength.toString();
|
fetchOptions.headers['Content-Length'] = contentLength.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessKey, unauthenticated } = options;
|
const { accessKey, basicAuth, unauthenticated } = options;
|
||||||
if (unauthenticated) {
|
if (basicAuth) {
|
||||||
|
fetchOptions.headers.Authorization = `Basic ${basicAuth}`;
|
||||||
|
} else if (unauthenticated) {
|
||||||
if (!accessKey) {
|
if (!accessKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'_promiseAjax: mode is aunathenticated, but accessKey was not provided'
|
'_promiseAjax: mode is aunathenticated, but accessKey was not provided'
|
||||||
|
@ -416,6 +428,7 @@ async function _promiseAjax(
|
||||||
resultPromise = response.textConverted();
|
resultPromise = response.textConverted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
return resultPromise.then(result => {
|
return resultPromise.then(result => {
|
||||||
if (
|
if (
|
||||||
options.responseType === 'arraybuffer' ||
|
options.responseType === 'arraybuffer' ||
|
||||||
|
@ -468,18 +481,29 @@ async function _promiseAjax(
|
||||||
} else {
|
} else {
|
||||||
window.log.info(options.type, url, response.status, 'Success');
|
window.log.info(options.type, url, response.status, 'Success');
|
||||||
}
|
}
|
||||||
if (
|
if (options.responseType === 'arraybufferwithdetails') {
|
||||||
options.responseType === 'arraybufferwithdetails' ||
|
const fullResult: ArrayBufferWithDetailsType = {
|
||||||
options.responseType === 'jsonwithdetails'
|
|
||||||
) {
|
|
||||||
resolve({
|
|
||||||
data: result,
|
data: result,
|
||||||
contentType: getContentType(response),
|
contentType: getContentType(response),
|
||||||
response,
|
response,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
resolve(fullResult);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (options.responseType === 'jsonwithdetails') {
|
||||||
|
const fullResult: JSONWithDetailsType = {
|
||||||
|
data: result,
|
||||||
|
contentType: getContentType(response),
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(fullResult);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -575,29 +599,32 @@ function makeHTTPError(
|
||||||
|
|
||||||
const URL_CALLS = {
|
const URL_CALLS = {
|
||||||
accounts: 'v1/accounts',
|
accounts: 'v1/accounts',
|
||||||
updateDeviceName: 'v1/accounts/name',
|
|
||||||
removeSignalingKey: 'v1/accounts/signaling_key',
|
|
||||||
getIceServers: 'v1/accounts/turn',
|
|
||||||
attachmentId: 'v2/attachments/form/upload',
|
attachmentId: 'v2/attachments/form/upload',
|
||||||
|
attestation: 'v1/attestation',
|
||||||
|
config: 'v1/config',
|
||||||
deliveryCert: 'v1/certificate/delivery',
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
devices: 'v1/devices',
|
devices: 'v1/devices',
|
||||||
|
directoryAuth: 'v1/directory/auth',
|
||||||
|
discovery: 'v1/discovery',
|
||||||
|
getGroupAvatarUpload: '/v1/groups/avatar/form',
|
||||||
|
getGroupCredentials: 'v1/certificate/group',
|
||||||
|
getIceServers: 'v1/accounts/turn',
|
||||||
|
getStickerPackUpload: 'v1/sticker/pack/form',
|
||||||
|
groupLog: 'v1/groups/logs',
|
||||||
|
groups: 'v1/groups',
|
||||||
keys: 'v2/keys',
|
keys: 'v2/keys',
|
||||||
messages: 'v1/messages',
|
messages: 'v1/messages',
|
||||||
profile: 'v1/profile',
|
profile: 'v1/profile',
|
||||||
registerCapabilities: 'v1/devices/capabilities',
|
registerCapabilities: 'v1/devices/capabilities',
|
||||||
|
removeSignalingKey: 'v1/accounts/signaling_key',
|
||||||
signed: 'v2/keys/signed',
|
signed: 'v2/keys/signed',
|
||||||
storageManifest: 'v1/storage/manifest',
|
storageManifest: 'v1/storage/manifest',
|
||||||
storageModify: 'v1/storage/',
|
storageModify: 'v1/storage/',
|
||||||
storageRead: 'v1/storage/read',
|
storageRead: 'v1/storage/read',
|
||||||
storageToken: 'v1/storage/auth',
|
storageToken: 'v1/storage/auth',
|
||||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||||
getStickerPackUpload: 'v1/sticker/pack/form',
|
updateDeviceName: 'v1/accounts/name',
|
||||||
whoami: 'v1/accounts/whoami',
|
whoami: 'v1/accounts/whoami',
|
||||||
config: 'v1/config',
|
|
||||||
directoryAuth: 'v1/directory/auth',
|
|
||||||
// CDS endpoints
|
|
||||||
attestation: 'v1/attestation',
|
|
||||||
discovery: 'v1/discovery',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type InitializeOptionsType = {
|
type InitializeOptionsType = {
|
||||||
|
@ -625,6 +652,7 @@ type MessageType = any;
|
||||||
|
|
||||||
type AjaxOptionsType = {
|
type AjaxOptionsType = {
|
||||||
accessKey?: string;
|
accessKey?: string;
|
||||||
|
basicAuth?: string;
|
||||||
call: keyof typeof URL_CALLS;
|
call: keyof typeof URL_CALLS;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
data?: ArrayBuffer | Buffer | string;
|
data?: ArrayBuffer | Buffer | string;
|
||||||
|
@ -648,6 +676,21 @@ export type WebAPIConnectType = {
|
||||||
|
|
||||||
type StickerPackManifestType = any;
|
type StickerPackManifestType = any;
|
||||||
|
|
||||||
|
export type GroupCredentialType = {
|
||||||
|
credential: string;
|
||||||
|
redemptionTime: number;
|
||||||
|
};
|
||||||
|
export type GroupCredentialsType = {
|
||||||
|
groupPublicParamsHex: string;
|
||||||
|
authCredentialPresentationHex: string;
|
||||||
|
};
|
||||||
|
export type GroupLogResponseType = {
|
||||||
|
currentRevision?: number;
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
changes: GroupChangesClass;
|
||||||
|
};
|
||||||
|
|
||||||
export type WebAPIType = {
|
export type WebAPIType = {
|
||||||
confirmCode: (
|
confirmCode: (
|
||||||
number: string,
|
number: string,
|
||||||
|
@ -657,9 +700,23 @@ export type WebAPIType = {
|
||||||
deviceName?: string | null,
|
deviceName?: string | null,
|
||||||
options?: { accessKey?: ArrayBuffer }
|
options?: { accessKey?: ArrayBuffer }
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
|
createGroup: (
|
||||||
|
group: GroupClass,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
) => Promise<void>;
|
||||||
getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>;
|
getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>;
|
||||||
getAvatar: (path: string) => Promise<any>;
|
getAvatar: (path: string) => Promise<any>;
|
||||||
getDevices: () => Promise<any>;
|
getDevices: () => Promise<any>;
|
||||||
|
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>;
|
||||||
|
getGroupAvatar: (key: string) => Promise<ArrayBuffer>;
|
||||||
|
getGroupCredentials: (
|
||||||
|
startDay: number,
|
||||||
|
endDay: number
|
||||||
|
) => Promise<Array<GroupCredentialType>>;
|
||||||
|
getGroupLog: (
|
||||||
|
startVersion: number,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
) => Promise<GroupLogResponseType>;
|
||||||
getIceServers: () => Promise<any>;
|
getIceServers: () => Promise<any>;
|
||||||
getKeysForIdentifier: (
|
getKeysForIdentifier: (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
|
@ -701,6 +758,10 @@ export type WebAPIType = {
|
||||||
targetUrl: string,
|
targetUrl: string,
|
||||||
options?: ProxiedRequestOptionsType
|
options?: ProxiedRequestOptionsType
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
|
modifyGroup: (
|
||||||
|
changes: GroupChangeClass.Actions,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
) => Promise<GroupChangeClass>;
|
||||||
modifyStorageRecords: MessageSender['modifyStorageRecords'];
|
modifyStorageRecords: MessageSender['modifyStorageRecords'];
|
||||||
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
|
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
|
||||||
registerCapabilities: (capabilities: any) => Promise<void>;
|
registerCapabilities: (capabilities: any) => Promise<void>;
|
||||||
|
@ -731,6 +792,10 @@ export type WebAPIType = {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
||||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||||
|
uploadGroupAvatar: (
|
||||||
|
avatarData: ArrayBuffer,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
) => Promise<string>;
|
||||||
whoami: () => Promise<any>;
|
whoami: () => Promise<any>;
|
||||||
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>;
|
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>;
|
||||||
};
|
};
|
||||||
|
@ -838,13 +903,20 @@ export function initialize({
|
||||||
let username = initialUsername;
|
let username = initialUsername;
|
||||||
let password = initialPassword;
|
let password = initialPassword;
|
||||||
const PARSE_RANGE_HEADER = /\/(\d+)$/;
|
const PARSE_RANGE_HEADER = /\/(\d+)$/;
|
||||||
|
const PARSE_GROUP_LOG_RANGE_HEADER = /$versions (\d{1,10})-(\d{1,10})\/(d{1,10})/;
|
||||||
|
|
||||||
// Thanks, function hoisting!
|
// Thanks, function hoisting!
|
||||||
return {
|
return {
|
||||||
confirmCode,
|
confirmCode,
|
||||||
|
createGroup,
|
||||||
getAttachment,
|
getAttachment,
|
||||||
getAvatar,
|
getAvatar,
|
||||||
|
getConfig,
|
||||||
getDevices,
|
getDevices,
|
||||||
|
getGroup,
|
||||||
|
getGroupAvatar,
|
||||||
|
getGroupCredentials,
|
||||||
|
getGroupLog,
|
||||||
getIceServers,
|
getIceServers,
|
||||||
getKeysForIdentifier,
|
getKeysForIdentifier,
|
||||||
getKeysForIdentifierUnauth,
|
getKeysForIdentifierUnauth,
|
||||||
|
@ -861,10 +933,11 @@ export function initialize({
|
||||||
getStorageRecords,
|
getStorageRecords,
|
||||||
getUuidsForE164s,
|
getUuidsForE164s,
|
||||||
makeProxiedRequest,
|
makeProxiedRequest,
|
||||||
|
modifyGroup,
|
||||||
modifyStorageRecords,
|
modifyStorageRecords,
|
||||||
putAttachment,
|
putAttachment,
|
||||||
registerCapabilities,
|
|
||||||
putStickers,
|
putStickers,
|
||||||
|
registerCapabilities,
|
||||||
registerKeys,
|
registerKeys,
|
||||||
registerSupportForUnauthenticatedDelivery,
|
registerSupportForUnauthenticatedDelivery,
|
||||||
removeSignalingKey,
|
removeSignalingKey,
|
||||||
|
@ -874,8 +947,8 @@ export function initialize({
|
||||||
sendMessagesUnauth,
|
sendMessagesUnauth,
|
||||||
setSignedPreKey,
|
setSignedPreKey,
|
||||||
updateDeviceName,
|
updateDeviceName,
|
||||||
|
uploadGroupAvatar,
|
||||||
whoami,
|
whoami,
|
||||||
getConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function _ajax(param: AjaxOptionsType): Promise<any> {
|
async function _ajax(param: AjaxOptionsType): Promise<any> {
|
||||||
|
@ -884,6 +957,7 @@ export function initialize({
|
||||||
}
|
}
|
||||||
|
|
||||||
return _outerAjax(null, {
|
return _outerAjax(null, {
|
||||||
|
basicAuth: param.basicAuth,
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
contentType: param.contentType || 'application/json; charset=utf-8',
|
contentType: param.contentType || 'application/json; charset=utf-8',
|
||||||
data: param.data || (param.jsonData && _jsonThing(param.jsonData)),
|
data: param.data || (param.jsonData && _jsonThing(param.jsonData)),
|
||||||
|
@ -1169,11 +1243,9 @@ export function initialize({
|
||||||
) {
|
) {
|
||||||
const { accessKey } = options;
|
const { accessKey } = options;
|
||||||
const jsonData: any = {
|
const jsonData: any = {
|
||||||
// tslint:disable-next-line: no-suspicious-comment
|
capabilities: {
|
||||||
// TODO: uncomment this once we want to start registering UUID support
|
gv2: true,
|
||||||
// capabilities: {
|
},
|
||||||
// uuid: true,
|
|
||||||
// },
|
|
||||||
fetchesMessages: true,
|
fetchesMessages: true,
|
||||||
name: deviceName ? deviceName : undefined,
|
name: deviceName ? deviceName : undefined,
|
||||||
registrationId,
|
registrationId,
|
||||||
|
@ -1695,13 +1767,13 @@ export function initialize({
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { response } = result;
|
const { response } = result as ArrayBufferWithDetailsType;
|
||||||
if (!response.headers || !response.headers.get) {
|
if (!response.headers || !response.headers.get) {
|
||||||
throw new Error('makeProxiedRequest: Problem retrieving header value');
|
throw new Error('makeProxiedRequest: Problem retrieving header value');
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = response.headers.get('content-range');
|
const range = response.headers.get('content-range');
|
||||||
const match = PARSE_RANGE_HEADER.exec(range);
|
const match = PARSE_RANGE_HEADER.exec(range || '');
|
||||||
|
|
||||||
if (!match || !match[1]) {
|
if (!match || !match[1]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -1717,6 +1789,228 @@ export function initialize({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Groups
|
||||||
|
|
||||||
|
function generateGroupAuth(
|
||||||
|
groupPublicParamsHex: string,
|
||||||
|
authCredentialPresentationHex: string
|
||||||
|
) {
|
||||||
|
return _btoa(`${groupPublicParamsHex}:${authCredentialPresentationHex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CredentialResponseType = {
|
||||||
|
credentials: Array<GroupCredentialType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getGroupCredentials(
|
||||||
|
startDay: number,
|
||||||
|
endDay: number
|
||||||
|
): Promise<Array<GroupCredentialType>> {
|
||||||
|
const response: CredentialResponseType = await _ajax({
|
||||||
|
call: 'getGroupCredentials',
|
||||||
|
urlParameters: `/${startDay}/${endDay}`,
|
||||||
|
httpType: 'GET',
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyAttributes(attributes: AvatarUploadAttributesClass) {
|
||||||
|
const {
|
||||||
|
key,
|
||||||
|
credential,
|
||||||
|
acl,
|
||||||
|
algorithm,
|
||||||
|
date,
|
||||||
|
policy,
|
||||||
|
signature,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!key ||
|
||||||
|
!credential ||
|
||||||
|
!acl ||
|
||||||
|
!algorithm ||
|
||||||
|
!date ||
|
||||||
|
!policy ||
|
||||||
|
!signature
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'verifyAttributes: Missing value from AvatarUploadAttributes'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
credential,
|
||||||
|
acl,
|
||||||
|
algorithm,
|
||||||
|
date,
|
||||||
|
policy,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadGroupAvatar(
|
||||||
|
avatarData: ArrayBuffer,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
): Promise<string> {
|
||||||
|
const basicAuth = generateGroupAuth(
|
||||||
|
options.groupPublicParamsHex,
|
||||||
|
options.authCredentialPresentationHex
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ArrayBuffer = await _ajax({
|
||||||
|
basicAuth,
|
||||||
|
call: 'getGroupAvatarUpload',
|
||||||
|
httpType: 'GET',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
host: storageUrl,
|
||||||
|
});
|
||||||
|
const attributes = window.textsecure.protobuf.AvatarUploadAttributes.decode(
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
const verified = verifyAttributes(attributes);
|
||||||
|
const { key } = verified;
|
||||||
|
|
||||||
|
const manifestParams = makePutParams(verified, avatarData);
|
||||||
|
|
||||||
|
await _outerAjax(`${cdnUrlObject['0']}/`, {
|
||||||
|
...manifestParams,
|
||||||
|
certificateAuthority,
|
||||||
|
proxyUrl,
|
||||||
|
timeout: 0,
|
||||||
|
type: 'POST',
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGroupAvatar(key: string): Promise<ArrayBuffer> {
|
||||||
|
return _outerAjax(`${cdnUrlObject['0']}/${key}`, {
|
||||||
|
certificateAuthority,
|
||||||
|
proxyUrl,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 0,
|
||||||
|
type: 'GET',
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGroup(
|
||||||
|
group: GroupClass,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
): Promise<void> {
|
||||||
|
const basicAuth = generateGroupAuth(
|
||||||
|
options.groupPublicParamsHex,
|
||||||
|
options.authCredentialPresentationHex
|
||||||
|
);
|
||||||
|
const data = group.toArrayBuffer();
|
||||||
|
|
||||||
|
await _ajax({
|
||||||
|
basicAuth,
|
||||||
|
call: 'groups',
|
||||||
|
httpType: 'PUT',
|
||||||
|
data,
|
||||||
|
host: storageUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGroup(
|
||||||
|
options: GroupCredentialsType
|
||||||
|
): Promise<GroupClass> {
|
||||||
|
const basicAuth = generateGroupAuth(
|
||||||
|
options.groupPublicParamsHex,
|
||||||
|
options.authCredentialPresentationHex
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ArrayBuffer = await _ajax({
|
||||||
|
basicAuth,
|
||||||
|
call: 'groups',
|
||||||
|
httpType: 'GET',
|
||||||
|
contentType: 'application/x-protobuf',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
host: storageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return window.textsecure.protobuf.Group.decode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function modifyGroup(
|
||||||
|
changes: GroupChangeClass.Actions,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
): Promise<GroupChangeClass> {
|
||||||
|
const basicAuth = generateGroupAuth(
|
||||||
|
options.groupPublicParamsHex,
|
||||||
|
options.authCredentialPresentationHex
|
||||||
|
);
|
||||||
|
const data = changes.toArrayBuffer();
|
||||||
|
|
||||||
|
const response: ArrayBuffer = await _ajax({
|
||||||
|
basicAuth,
|
||||||
|
call: 'groups',
|
||||||
|
httpType: 'PATCH',
|
||||||
|
data,
|
||||||
|
contentType: 'application/x-protobuf',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
host: storageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return window.textsecure.protobuf.GroupChange.decode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGroupLog(
|
||||||
|
startVersion: number,
|
||||||
|
options: GroupCredentialsType
|
||||||
|
): Promise<GroupLogResponseType> {
|
||||||
|
const basicAuth = generateGroupAuth(
|
||||||
|
options.groupPublicParamsHex,
|
||||||
|
options.authCredentialPresentationHex
|
||||||
|
);
|
||||||
|
|
||||||
|
const withDetails: ArrayBufferWithDetailsType = await _ajax({
|
||||||
|
basicAuth,
|
||||||
|
call: 'groupLog',
|
||||||
|
urlParameters: `/${startVersion}`,
|
||||||
|
httpType: 'GET',
|
||||||
|
contentType: 'application/x-protobuf',
|
||||||
|
responseType: 'arraybufferwithdetails',
|
||||||
|
host: storageUrl,
|
||||||
|
});
|
||||||
|
const { data, response } = withDetails;
|
||||||
|
const changes = window.textsecure.protobuf.GroupChanges.decode(data);
|
||||||
|
|
||||||
|
if (response && response.status === 206) {
|
||||||
|
const range = response.headers.get('Content-Range');
|
||||||
|
const match = PARSE_GROUP_LOG_RANGE_HEADER.exec(range || '');
|
||||||
|
|
||||||
|
const start = match ? parseInt(match[0], 10) : undefined;
|
||||||
|
const end = match ? parseInt(match[1], 10) : undefined;
|
||||||
|
const currentRevision = match ? parseInt(match[2], 10) : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
match &&
|
||||||
|
is.number(start) &&
|
||||||
|
is.number(end) &&
|
||||||
|
is.number(currentRevision)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
changes,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
currentRevision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
changes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getMessageSocket() {
|
function getMessageSocket() {
|
||||||
window.log.info('opening message socket', url);
|
window.log.info('opening message socket', url);
|
||||||
const fixedScheme = url
|
const fixedScheme = url
|
||||||
|
|
|
@ -10,7 +10,7 @@ export async function deleteForEveryone(
|
||||||
// Make sure the server timestamps for the DOE and the matching message
|
// Make sure the server timestamps for the DOE and the matching message
|
||||||
// are less than one day apart
|
// are less than one day apart
|
||||||
const delta = Math.abs(
|
const delta = Math.abs(
|
||||||
doe.get('serverTimestamp') - message.get('serverTimestamp')
|
doe.get('serverTimestamp') - (message.get('serverTimestamp') || 0)
|
||||||
);
|
);
|
||||||
if (delta > ONE_DAY) {
|
if (delta > ONE_DAY) {
|
||||||
window.log.info('Received late DOE. Dropping.', {
|
window.log.info('Received late DOE. Dropping.', {
|
||||||
|
|
|
@ -12852,38 +12852,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-04-05T23:45:16.746Z"
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "ts/textsecure/SendMessage.js",
|
|
||||||
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
|
||||||
"lineNumber": 25,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-04-05T23:45:16.746Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "ts/textsecure/SendMessage.js",
|
|
||||||
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
|
||||||
"lineNumber": 28,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-04-05T23:45:16.746Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "ts/textsecure/SendMessage.ts",
|
|
||||||
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
|
||||||
"lineNumber": 32,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-05-28T18:08:02.658Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "ts/textsecure/SendMessage.ts",
|
|
||||||
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
|
||||||
"lineNumber": 35,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-05-28T18:08:02.658Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "ts/textsecure/SyncRequest.js",
|
"path": "ts/textsecure/SyncRequest.js",
|
||||||
|
@ -12952,7 +12920,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "ts/textsecure/WebAPI.js",
|
"path": "ts/textsecure/WebAPI.js",
|
||||||
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);",
|
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);",
|
||||||
"lineNumber": 1057,
|
"lineNumber": 1213,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-08T23:07:22.682Z"
|
"updated": "2020-09-08T23:07:22.682Z"
|
||||||
},
|
},
|
||||||
|
@ -12960,8 +12928,8 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "ts/textsecure/WebAPI.ts",
|
"path": "ts/textsecure/WebAPI.ts",
|
||||||
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
|
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
|
||||||
"lineNumber": 1769,
|
"lineNumber": 2063,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-08T23:07:22.682Z"
|
"updated": "2020-09-08T23:07:22.682Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -1,13 +1,21 @@
|
||||||
export * from 'zkgroup';
|
export * from 'zkgroup';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AuthCredential,
|
||||||
|
ClientZkAuthOperations,
|
||||||
|
ClientZkGroupCipher,
|
||||||
ClientZkProfileOperations,
|
ClientZkProfileOperations,
|
||||||
FFICompatArray,
|
FFICompatArray,
|
||||||
FFICompatArrayType,
|
FFICompatArrayType,
|
||||||
|
GroupMasterKey,
|
||||||
|
GroupSecretParams,
|
||||||
ProfileKey,
|
ProfileKey,
|
||||||
|
ProfileKeyCiphertext,
|
||||||
|
ProfileKeyCredentialPresentation,
|
||||||
ProfileKeyCredentialRequestContext,
|
ProfileKeyCredentialRequestContext,
|
||||||
ProfileKeyCredentialResponse,
|
ProfileKeyCredentialResponse,
|
||||||
ServerPublicParams,
|
ServerPublicParams,
|
||||||
|
UuidCiphertext,
|
||||||
} from 'zkgroup';
|
} from 'zkgroup';
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
|
@ -16,6 +24,8 @@ import {
|
||||||
typedArrayToArrayBuffer,
|
typedArrayToArrayBuffer,
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
|
|
||||||
|
// Simple utility functions
|
||||||
|
|
||||||
export function arrayBufferToCompatArray(
|
export function arrayBufferToCompatArray(
|
||||||
arrayBuffer: ArrayBuffer
|
arrayBuffer: ArrayBuffer
|
||||||
): FFICompatArrayType {
|
): FFICompatArrayType {
|
||||||
|
@ -42,6 +52,68 @@ export function compatArrayToHex(compatArray: FFICompatArrayType): string {
|
||||||
return arrayBufferToHex(compatArrayToArrayBuffer(compatArray));
|
return arrayBufferToHex(compatArrayToArrayBuffer(compatArray));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scenarios
|
||||||
|
|
||||||
|
export function decryptGroupBlob(
|
||||||
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
|
ciphertext: ArrayBuffer
|
||||||
|
) {
|
||||||
|
return compatArrayToArrayBuffer(
|
||||||
|
clientZkGroupCipher.decryptBlob(arrayBufferToCompatArray(ciphertext))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptProfileKeyCredentialPresentation(
|
||||||
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
|
presentationBuffer: ArrayBuffer
|
||||||
|
): { profileKey: ArrayBuffer; uuid: string } {
|
||||||
|
const presentation = new ProfileKeyCredentialPresentation(
|
||||||
|
arrayBufferToCompatArray(presentationBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
const uuidCiphertext = presentation.getUuidCiphertext();
|
||||||
|
const uuid = clientZkGroupCipher.decryptUuid(uuidCiphertext);
|
||||||
|
|
||||||
|
const profileKeyCiphertext = presentation.getProfileKeyCiphertext();
|
||||||
|
const profileKey = clientZkGroupCipher.decryptProfileKey(
|
||||||
|
profileKeyCiphertext,
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profileKey: compatArrayToArrayBuffer(profileKey.serialize()),
|
||||||
|
uuid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptProfileKey(
|
||||||
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
|
profileKeyCiphertextBuffer: ArrayBuffer,
|
||||||
|
uuid: string
|
||||||
|
): ArrayBuffer {
|
||||||
|
const profileKeyCiphertext = new ProfileKeyCiphertext(
|
||||||
|
arrayBufferToCompatArray(profileKeyCiphertextBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
const profileKey = clientZkGroupCipher.decryptProfileKey(
|
||||||
|
profileKeyCiphertext,
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
return compatArrayToArrayBuffer(profileKey.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptUuid(
|
||||||
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
|
uuidCiphertextBuffer: ArrayBuffer
|
||||||
|
): string {
|
||||||
|
const uuidCiphertext = new UuidCiphertext(
|
||||||
|
arrayBufferToCompatArray(uuidCiphertextBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
return clientZkGroupCipher.decryptUuid(uuidCiphertext);
|
||||||
|
}
|
||||||
|
|
||||||
export function deriveProfileKeyVersion(
|
export function deriveProfileKeyVersion(
|
||||||
profileKeyBase64: string,
|
profileKeyBase64: string,
|
||||||
uuid: string
|
uuid: string
|
||||||
|
@ -54,13 +126,56 @@ export function deriveProfileKeyVersion(
|
||||||
return profileKeyVersion.toString();
|
return profileKeyVersion.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClientZkProfileOperations(
|
export function deriveGroupPublicParams(groupSecretParamsBuffer: ArrayBuffer) {
|
||||||
serverPublicParamsBase64: string
|
const groupSecretParams = new GroupSecretParams(
|
||||||
): ClientZkProfileOperations {
|
arrayBufferToCompatArray(groupSecretParamsBuffer)
|
||||||
const serverPublicParamsArray = base64ToCompatArray(serverPublicParamsBase64);
|
);
|
||||||
const serverPublicParams = new ServerPublicParams(serverPublicParamsArray);
|
|
||||||
|
|
||||||
return new ClientZkProfileOperations(serverPublicParams);
|
return compatArrayToArrayBuffer(
|
||||||
|
groupSecretParams.getPublicParams().serialize()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveGroupID(groupSecretParamsBuffer: ArrayBuffer) {
|
||||||
|
const groupSecretParams = new GroupSecretParams(
|
||||||
|
arrayBufferToCompatArray(groupSecretParamsBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
return compatArrayToArrayBuffer(
|
||||||
|
groupSecretParams
|
||||||
|
.getPublicParams()
|
||||||
|
.getGroupIdentifier()
|
||||||
|
.serialize()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveGroupSecretParams(
|
||||||
|
masterKeyBuffer: ArrayBuffer
|
||||||
|
): ArrayBuffer {
|
||||||
|
const masterKey = new GroupMasterKey(
|
||||||
|
arrayBufferToCompatArray(masterKeyBuffer)
|
||||||
|
);
|
||||||
|
const groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
|
||||||
|
|
||||||
|
return compatArrayToArrayBuffer(groupSecretParams.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptGroupBlob(
|
||||||
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
|
plaintext: ArrayBuffer
|
||||||
|
) {
|
||||||
|
return compatArrayToArrayBuffer(
|
||||||
|
clientZkGroupCipher.encryptBlob(arrayBufferToCompatArray(plaintext))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptUuid(
|
||||||
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
|
uuidPlaintext: string
|
||||||
|
): ArrayBuffer {
|
||||||
|
const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext);
|
||||||
|
|
||||||
|
return compatArrayToArrayBuffer(uuidCiphertext.serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateProfileKeyCredentialRequest(
|
export function generateProfileKeyCredentialRequest(
|
||||||
|
@ -84,13 +199,63 @@ export function generateProfileKeyCredentialRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAuthCredentialPresentation(
|
||||||
|
clientZkAuthOperations: ClientZkAuthOperations,
|
||||||
|
authCredentialBase64: string,
|
||||||
|
groupSecretParamsBase64: string
|
||||||
|
) {
|
||||||
|
const authCredential = new AuthCredential(
|
||||||
|
base64ToCompatArray(authCredentialBase64)
|
||||||
|
);
|
||||||
|
const secretParams = new GroupSecretParams(
|
||||||
|
base64ToCompatArray(groupSecretParamsBase64)
|
||||||
|
);
|
||||||
|
|
||||||
|
const presentation = clientZkAuthOperations.createAuthCredentialPresentation(
|
||||||
|
secretParams,
|
||||||
|
authCredential
|
||||||
|
);
|
||||||
|
return compatArrayToArrayBuffer(presentation.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientZkAuthOperations(
|
||||||
|
serverPublicParamsBase64: string
|
||||||
|
): ClientZkAuthOperations {
|
||||||
|
const serverPublicParams = new ServerPublicParams(
|
||||||
|
base64ToCompatArray(serverPublicParamsBase64)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ClientZkAuthOperations(serverPublicParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientZkGroupCipher(
|
||||||
|
groupSecretParamsBase64: string
|
||||||
|
): ClientZkGroupCipher {
|
||||||
|
const serverPublicParams = new GroupSecretParams(
|
||||||
|
base64ToCompatArray(groupSecretParamsBase64)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ClientZkGroupCipher(serverPublicParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientZkProfileOperations(
|
||||||
|
serverPublicParamsBase64: string
|
||||||
|
): ClientZkProfileOperations {
|
||||||
|
const serverPublicParams = new ServerPublicParams(
|
||||||
|
base64ToCompatArray(serverPublicParamsBase64)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ClientZkProfileOperations(serverPublicParams);
|
||||||
|
}
|
||||||
|
|
||||||
export function handleProfileKeyCredential(
|
export function handleProfileKeyCredential(
|
||||||
clientZkProfileCipher: ClientZkProfileOperations,
|
clientZkProfileCipher: ClientZkProfileOperations,
|
||||||
context: ProfileKeyCredentialRequestContext,
|
context: ProfileKeyCredentialRequestContext,
|
||||||
responseBase64: string
|
responseBase64: string
|
||||||
): string {
|
): string {
|
||||||
const responseArray = base64ToCompatArray(responseBase64);
|
const response = new ProfileKeyCredentialResponse(
|
||||||
const response = new ProfileKeyCredentialResponse(responseArray);
|
base64ToCompatArray(responseBase64)
|
||||||
|
);
|
||||||
const profileKeyCredential = clientZkProfileCipher.receiveProfileKeyCredential(
|
const profileKeyCredential = clientZkProfileCipher.receiveProfileKeyCredential(
|
||||||
context,
|
context,
|
||||||
response
|
response
|
||||||
|
|
19
ts/window.d.ts
vendored
19
ts/window.d.ts
vendored
|
@ -23,6 +23,7 @@ import { CallHistoryDetailsType } from './types/Calling';
|
||||||
import { ColorType } from './types/Colors';
|
import { ColorType } from './types/Colors';
|
||||||
import { ConversationController } from './ConversationController';
|
import { ConversationController } from './ConversationController';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
|
import AccountManager from './textsecure/AccountManager';
|
||||||
import Data from './sql/Client';
|
import Data from './sql/Client';
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
@ -32,6 +33,7 @@ type TaskResultType = any;
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
dcodeIO: DCodeIOType;
|
dcodeIO: DCodeIOType;
|
||||||
|
getAccountManager: () => AccountManager | undefined;
|
||||||
getAlwaysRelayCalls: () => Promise<boolean>;
|
getAlwaysRelayCalls: () => Promise<boolean>;
|
||||||
getCallRingtoneNotification: () => Promise<boolean>;
|
getCallRingtoneNotification: () => Promise<boolean>;
|
||||||
getCallSystemNotification: () => Promise<boolean>;
|
getCallSystemNotification: () => Promise<boolean>;
|
||||||
|
@ -43,8 +45,10 @@ declare global {
|
||||||
getIncomingCallNotification: () => Promise<boolean>;
|
getIncomingCallNotification: () => Promise<boolean>;
|
||||||
getMediaCameraPermissions: () => Promise<boolean>;
|
getMediaCameraPermissions: () => Promise<boolean>;
|
||||||
getMediaPermissions: () => Promise<boolean>;
|
getMediaPermissions: () => Promise<boolean>;
|
||||||
|
getServerPublicParams: () => string;
|
||||||
getSocketStatus: () => number;
|
getSocketStatus: () => number;
|
||||||
getTitle: () => string;
|
getTitle: () => string;
|
||||||
|
waitForEmptyEventQueue: () => Promise<void>;
|
||||||
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isValidGuid: (maybeGuid: string) => boolean;
|
isValidGuid: (maybeGuid: string) => boolean;
|
||||||
|
@ -88,13 +92,24 @@ declare global {
|
||||||
Services: {
|
Services: {
|
||||||
calling: CallingClass;
|
calling: CallingClass;
|
||||||
};
|
};
|
||||||
|
Migrations: {
|
||||||
|
deleteAttachmentData: (path: string) => Promise<void>;
|
||||||
|
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||||
|
};
|
||||||
|
Types: {
|
||||||
|
Message: {
|
||||||
|
CURRENT_SCHEMA_VERSION: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
ConversationController: ConversationController;
|
ConversationController: ConversationController;
|
||||||
|
MessageController: MessageControllerType;
|
||||||
WebAPI: WebAPIConnectType;
|
WebAPI: WebAPIConnectType;
|
||||||
Whisper: WhisperType;
|
Whisper: WhisperType;
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
CALLING: boolean;
|
CALLING: boolean;
|
||||||
|
GV2: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Error {
|
interface Error {
|
||||||
|
@ -114,6 +129,10 @@ export type DCodeIOType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MessageControllerType = {
|
||||||
|
register: (id: string, model: MessageModelType) => MessageModelType;
|
||||||
|
};
|
||||||
|
|
||||||
export class CertificateValidatorType {
|
export class CertificateValidatorType {
|
||||||
validate: (cerficate: any, certificateTime: number) => Promise<void>;
|
validate: (cerficate: any, certificateTime: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue