GroupV2: Show summary of change details on re-join of group
This commit is contained in:
parent
105162dc66
commit
e9b7a74b32
6 changed files with 181 additions and 42 deletions
|
@ -4099,6 +4099,10 @@
|
|||
"message": "The group was changed to allow all members to send messages.",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--summary": {
|
||||
"messageformat": "This group's members or settings have changed.",
|
||||
"description": "When rejoining a group, any detected changes are collapsed down into this summary"
|
||||
},
|
||||
"GroupV1--Migration--disabled": {
|
||||
"message": "Upgrade this group to activate new features like @mentions and admins. Members who have not shared their name or photo in this group will be invited to join. $learnMore$",
|
||||
"description": "Shown instead of composition area when user is forced to migrate a legacy group (GV1)."
|
||||
|
|
|
@ -156,6 +156,13 @@
|
|||
);
|
||||
}
|
||||
|
||||
&--icon-group-summary::before {
|
||||
@include system-message-icon(
|
||||
'../images/icons/v2/info-16.svg',
|
||||
'../images/icons/v2/info-16.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--icon-info::before {
|
||||
@include system-message-icon(
|
||||
'../images/icons/v2/info-16.svg',
|
||||
|
|
|
@ -1278,14 +1278,45 @@ export function AdminApprovalRemove(): JSX.Element {
|
|||
},
|
||||
],
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AdminApprovalRemove.story = {
|
||||
name: 'Admin Approval (Remove)',
|
||||
};
|
||||
|
||||
export function AdminApprovalBounce(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
Should show button:
|
||||
{renderChange(
|
||||
{
|
||||
// From Joiner
|
||||
from: CONTACT_A,
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-remove-one',
|
||||
type: 'admin-approval-bounce',
|
||||
uuid: CONTACT_A,
|
||||
times: 1,
|
||||
isApprovalPending: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupMemberships: [{ uuid: CONTACT_C, isAdmin: false }],
|
||||
groupBannedMemberships: [CONTACT_B],
|
||||
}
|
||||
)}
|
||||
{renderChange(
|
||||
{
|
||||
// From nobody
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-bounce',
|
||||
uuid: CONTACT_A,
|
||||
times: 1,
|
||||
isApprovalPending: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1295,37 +1326,26 @@ export function AdminApprovalRemove(): JSX.Element {
|
|||
}
|
||||
)}
|
||||
{renderChange({
|
||||
from: ADMIN_A,
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-remove-one',
|
||||
type: 'admin-approval-bounce',
|
||||
uuid: CONTACT_A,
|
||||
times: 1,
|
||||
isApprovalPending: false,
|
||||
},
|
||||
// No group membership info
|
||||
],
|
||||
})}
|
||||
Should show button:
|
||||
{renderChange(
|
||||
{
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-remove-one',
|
||||
uuid: CONTACT_A,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupMemberships: [{ uuid: CONTACT_C, isAdmin: false }],
|
||||
groupBannedMemberships: [CONTACT_B],
|
||||
}
|
||||
)}
|
||||
Would show button, but we're not admin:
|
||||
{renderChange(
|
||||
{
|
||||
from: CONTACT_A,
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-remove-one',
|
||||
type: 'admin-approval-bounce',
|
||||
uuid: CONTACT_A,
|
||||
times: 1,
|
||||
isApprovalPending: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1338,8 +1358,10 @@ export function AdminApprovalRemove(): JSX.Element {
|
|||
from: CONTACT_A,
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-remove-one',
|
||||
type: 'admin-approval-bounce',
|
||||
uuid: CONTACT_A,
|
||||
times: 1,
|
||||
isApprovalPending: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1351,8 +1373,10 @@ export function AdminApprovalRemove(): JSX.Element {
|
|||
from: CONTACT_A,
|
||||
details: [
|
||||
{
|
||||
type: 'admin-approval-remove-one',
|
||||
type: 'admin-approval-bounce',
|
||||
uuid: CONTACT_A,
|
||||
times: 1,
|
||||
isApprovalPending: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1363,8 +1387,8 @@ export function AdminApprovalRemove(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
AdminApprovalRemove.story = {
|
||||
name: 'Admin Approval (Remove)',
|
||||
AdminApprovalBounce.story = {
|
||||
name: 'Admin Approval (Bounce)',
|
||||
};
|
||||
|
||||
export function GroupLinkAdd(): JSX.Element {
|
||||
|
@ -1646,3 +1670,18 @@ export function AnnouncementGroupChange(): JSX.Element {
|
|||
AnnouncementGroupChange.story = {
|
||||
name: 'Announcement Group (Change)',
|
||||
};
|
||||
|
||||
export function Summary(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{renderChange({
|
||||
from: OUR_ACI,
|
||||
details: [
|
||||
{
|
||||
type: 'summary',
|
||||
},
|
||||
],
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ type GroupIconType =
|
|||
| 'group-avatar'
|
||||
| 'group-decline'
|
||||
| 'group-edit'
|
||||
| 'group-summary'
|
||||
| 'group-leave'
|
||||
| 'group-remove';
|
||||
|
||||
|
@ -120,6 +121,9 @@ function getIcon(
|
|||
if (changeType === 'admin-approval-bounce' && isLastText) {
|
||||
possibleIcon = undefined;
|
||||
}
|
||||
if (changeType === 'summary') {
|
||||
possibleIcon = 'group-summary';
|
||||
}
|
||||
return possibleIcon || 'group';
|
||||
}
|
||||
|
||||
|
|
|
@ -925,6 +925,9 @@ export function renderChangeDetail<T>(
|
|||
}
|
||||
return renderString('GroupV2--announcements--member--unknown', i18n);
|
||||
}
|
||||
if (detail.type === 'summary') {
|
||||
return renderString('icu:GroupV2--summary', i18n);
|
||||
}
|
||||
|
||||
throw missingCaseError(detail);
|
||||
}
|
||||
|
|
120
ts/groups.ts
120
ts/groups.ts
|
@ -202,6 +202,9 @@ export type GroupV2DescriptionChangeType = {
|
|||
// Adding this field; cannot remove previous field for backwards compatibility
|
||||
description?: string;
|
||||
};
|
||||
export type GroupV2SummaryType = {
|
||||
type: 'summary';
|
||||
};
|
||||
|
||||
export type GroupV2ChangeDetailType =
|
||||
| GroupV2AccessAttributesChangeType
|
||||
|
@ -227,6 +230,7 @@ export type GroupV2ChangeDetailType =
|
|||
| GroupV2PendingAddOneChangeType
|
||||
| GroupV2PendingRemoveManyChangeType
|
||||
| GroupV2PendingRemoveOneChangeType
|
||||
| GroupV2SummaryType
|
||||
| GroupV2TitleChangeType;
|
||||
|
||||
export type GroupV2ChangeType = {
|
||||
|
@ -3658,32 +3662,62 @@ async function updateGroupViaSingleChange({
|
|||
groupChange: Proto.IGroupChange;
|
||||
newRevision: number;
|
||||
}): Promise<UpdatesResultType> {
|
||||
const previouslyKnewAboutThisGroup =
|
||||
isNumber(group.revision) && group.membersV2?.length;
|
||||
const wasInGroup = !group.left;
|
||||
const result: UpdatesResultType = await integrateGroupChange({
|
||||
const singleChangeResult: UpdatesResultType = await integrateGroupChange({
|
||||
group,
|
||||
groupChange,
|
||||
newRevision,
|
||||
});
|
||||
|
||||
const nowInGroup = !result.newAttributes.left;
|
||||
const nowInGroup = !singleChangeResult.newAttributes.left;
|
||||
|
||||
// If we were just added to the group (for example, via a join link), we go fetch the
|
||||
// entire group state to make sure we're up to date.
|
||||
if (!wasInGroup && nowInGroup) {
|
||||
const { newAttributes, members } = await updateGroupViaState({
|
||||
group: result.newAttributes,
|
||||
const {
|
||||
newAttributes,
|
||||
members,
|
||||
groupChangeMessages: catchupMessages,
|
||||
} = await updateGroupViaState({
|
||||
group: singleChangeResult.newAttributes,
|
||||
});
|
||||
|
||||
const groupChangeMessages = [...singleChangeResult.groupChangeMessages];
|
||||
// If we've just been added to a group we were previously in, we do want to show
|
||||
// a summary instead of nothing.
|
||||
if (
|
||||
groupChangeMessages.length > 0 &&
|
||||
previouslyKnewAboutThisGroup &&
|
||||
catchupMessages.length > 0
|
||||
) {
|
||||
groupChangeMessages.push({
|
||||
...generateBasicMessage(),
|
||||
type: 'group-v2-change',
|
||||
groupV2Change: {
|
||||
details: [
|
||||
{
|
||||
type: 'summary',
|
||||
},
|
||||
],
|
||||
},
|
||||
readStatus: ReadStatus.Read,
|
||||
// For simplicity, since we don't know who this change is from here, always Seen
|
||||
seenStatus: SeenStatus.Seen,
|
||||
});
|
||||
}
|
||||
|
||||
// We discard any change events that come out of this full group fetch, but we do
|
||||
// keep the final group attributes generated, as well as any new members.
|
||||
return {
|
||||
...result,
|
||||
members: [...result.members, ...members],
|
||||
groupChangeMessages,
|
||||
members: [...singleChangeResult.members, ...members],
|
||||
newAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
return singleChangeResult;
|
||||
}
|
||||
|
||||
async function updateGroupViaLogs({
|
||||
|
@ -4170,6 +4204,21 @@ function extractDiffs({
|
|||
let areWePendingApproval = false;
|
||||
let whoInvitedUsUserId = null;
|
||||
|
||||
function isUs(uuid: UUIDStringType): boolean {
|
||||
return uuid === ourACI.toString() || uuid === ourPNI?.toString();
|
||||
}
|
||||
function keepOnlyOurAdds(
|
||||
list: Array<GroupV2ChangeDetailType>
|
||||
): Array<GroupV2ChangeDetailType> {
|
||||
return list.filter(
|
||||
item =>
|
||||
(item.type === 'member-add-from-invite' && isUs(item.uuid)) ||
|
||||
(item.type === 'member-add-from-link' && isUs(item.uuid)) ||
|
||||
(item.type === 'member-add-from-admin-approval' && isUs(item.uuid)) ||
|
||||
(item.type === 'member-add' && isUs(item.uuid))
|
||||
);
|
||||
}
|
||||
|
||||
// access control
|
||||
|
||||
if (
|
||||
|
@ -4274,6 +4323,10 @@ function extractDiffs({
|
|||
const oldMemberLookup = new Map<UUIDStringType, GroupV2MemberType>(
|
||||
(old.membersV2 || []).map(member => [member.uuid, member])
|
||||
);
|
||||
const didWeStartInGroup =
|
||||
(ourACI && oldMemberLookup.get(ourACI.toString())) ||
|
||||
(ourPNI && oldMemberLookup.get(ourPNI.toString()));
|
||||
|
||||
const oldPendingMemberLookup = new Map<
|
||||
UUIDStringType,
|
||||
GroupV2PendingMemberType
|
||||
|
@ -4288,16 +4341,16 @@ function extractDiffs({
|
|||
|
||||
(current.membersV2 || []).forEach(currentMember => {
|
||||
const { uuid } = currentMember;
|
||||
const isUs = uuid === ourACI.toString();
|
||||
const uuidIsUs = isUs(uuid);
|
||||
|
||||
if (isUs) {
|
||||
if (uuidIsUs) {
|
||||
areWeInGroup = true;
|
||||
}
|
||||
|
||||
const oldMember = oldMemberLookup.get(uuid);
|
||||
if (!oldMember) {
|
||||
let pendingMember = oldPendingMemberLookup.get(uuid);
|
||||
if (isUs && ourPNI && !pendingMember) {
|
||||
if (uuidIsUs && ourPNI && !pendingMember) {
|
||||
pendingMember = oldPendingMemberLookup.get(ourPNI.toString());
|
||||
}
|
||||
if (pendingMember) {
|
||||
|
@ -4347,7 +4400,7 @@ function extractDiffs({
|
|||
// pretend that the PNI wasn't pending so that we won't generate a
|
||||
// pending-add-one notification below.
|
||||
if (
|
||||
isUs &&
|
||||
uuidIsUs &&
|
||||
ourPNI &&
|
||||
!oldMember &&
|
||||
oldPendingMemberLookup.has(ourPNI.toString()) &&
|
||||
|
@ -4373,7 +4426,7 @@ function extractDiffs({
|
|||
const { uuid } = currentPendingMember;
|
||||
const oldPendingMember = oldPendingMemberLookup.get(uuid);
|
||||
|
||||
if (uuid === ourACI.toString() || uuid === ourPNI?.toString()) {
|
||||
if (isUs(uuid)) {
|
||||
if (uuid === ourACI.toString()) {
|
||||
uuidKindInvitedToGroup = UUIDKind.ACI;
|
||||
} else if (uuidKindInvitedToGroup === undefined) {
|
||||
|
@ -4494,8 +4547,9 @@ function extractDiffs({
|
|||
|
||||
const firstUpdate = !isNumber(old.revision);
|
||||
const isFromUs = ourACI.toString() === sourceUuid;
|
||||
const justJoinedGroup = !firstUpdate && !didWeStartInGroup && areWeInGroup;
|
||||
|
||||
// Here we hardcode initial messages if this is our first time processing data this
|
||||
// Here we hardcode initial messages if this is our first time processing data for this
|
||||
// group. Ideally we can collapse it down to just one of: 'you were added',
|
||||
// 'you were invited', or 'you created.'
|
||||
if (firstUpdate && uuidKindInvitedToGroup !== undefined) {
|
||||
|
@ -4554,17 +4608,19 @@ function extractDiffs({
|
|||
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||
};
|
||||
} else if (firstUpdate && areWeInGroup) {
|
||||
const filteredDetails = keepOnlyOurAdds(details);
|
||||
|
||||
strictAssert(
|
||||
filteredDetails.length === 1,
|
||||
'extractDiffs/firstUpdate: Should be only one self-add!'
|
||||
);
|
||||
|
||||
message = {
|
||||
...generateBasicMessage(),
|
||||
type: 'group-v2-change',
|
||||
groupV2Change: {
|
||||
from: sourceUuid,
|
||||
details: [
|
||||
{
|
||||
type: 'member-add',
|
||||
uuid: ourACI.toString(),
|
||||
},
|
||||
],
|
||||
details: filteredDetails,
|
||||
},
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||
|
@ -4584,6 +4640,32 @@ function extractDiffs({
|
|||
readStatus: ReadStatus.Read,
|
||||
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||
};
|
||||
} else if (justJoinedGroup) {
|
||||
const filteredDetails = keepOnlyOurAdds(details);
|
||||
|
||||
strictAssert(
|
||||
filteredDetails.length === 1,
|
||||
'extractDiffs/justJoinedGroup: Should be only one self-add!'
|
||||
);
|
||||
|
||||
// If we've dropped other changes, we collapse them into a single summary
|
||||
if (details.length > 1) {
|
||||
filteredDetails.push({
|
||||
type: 'summary',
|
||||
});
|
||||
}
|
||||
|
||||
message = {
|
||||
...generateBasicMessage(),
|
||||
type: 'group-v2-change',
|
||||
sourceUuid,
|
||||
groupV2Change: {
|
||||
from: sourceUuid,
|
||||
details: filteredDetails,
|
||||
},
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: isFromUs ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||
};
|
||||
} else if (details.length > 0) {
|
||||
message = {
|
||||
...generateBasicMessage(),
|
||||
|
|
Loading…
Reference in a new issue