mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-17 13:20:23 +01:00
Track verified group name hash
This commit is contained in:
@@ -174,6 +174,7 @@ message GroupV2Record {
|
||||
reserved 9;
|
||||
StorySendMode storySendMode = 10;
|
||||
optional AvatarColor avatarColor = 11;
|
||||
bytes verifiedNameHash = 12; // SHA-256 of UTF-8 encoded decrypted group title that was last verified
|
||||
}
|
||||
|
||||
message Payments {
|
||||
@@ -232,12 +233,6 @@ message AccountRecord {
|
||||
}
|
||||
}
|
||||
|
||||
message BackupTierHistory {
|
||||
// See zkgroup for integer particular values. Unset if backups are not enabled.
|
||||
optional uint64 backupTier = 1;
|
||||
optional uint64 endedAtTimestamp = 2;
|
||||
}
|
||||
|
||||
message NotificationProfileManualOverride {
|
||||
message ManuallyEnabled {
|
||||
bytes id = 1;
|
||||
@@ -293,12 +288,12 @@ message AccountRecord {
|
||||
reserved /*backupsSubscriberId*/ 36;
|
||||
reserved /*backupsSubscriberCurrencyCode*/ 37;
|
||||
reserved /*backupsSubscriptionManuallyCancelled*/ 38;
|
||||
// Set to true after backups are enabled and one is uploaded.
|
||||
reserved /*hasBackup*/ 39;
|
||||
// See zkgroup for integer particular values. Unset if backups are not enabled.
|
||||
optional uint64 backupTier = 40;
|
||||
IAPSubscriberData backupSubscriberData = 41;
|
||||
optional AvatarColor avatarColor = 42;
|
||||
reserved 43; // backupTierHistory
|
||||
NotificationProfileManualOverride notificationProfileManualOverride = 44;
|
||||
bool notificationProfileSyncDisabled = 45;
|
||||
bool automaticKeyVerificationDisabled = 46;
|
||||
@@ -339,9 +334,8 @@ message StickerPackRecord {
|
||||
message CallLinkRecord {
|
||||
bytes rootKey = 1; // 16 bytes
|
||||
bytes adminPasskey = 2; // Non-empty when the current user is an admin
|
||||
uint64 deletedAtTimestampMs = 3; // When present and non-zero, `adminPasskey`
|
||||
// should be cleared
|
||||
reserved 4; // was epoch, never used
|
||||
uint64 deletedAtTimestampMs = 3; // When present and non-zero, `adminPasskey` should be cleared
|
||||
reserved /*epoch*/ 4;
|
||||
}
|
||||
|
||||
message Recipient {
|
||||
|
||||
@@ -48,10 +48,10 @@ export default {
|
||||
component: ConversationHero,
|
||||
args: {
|
||||
conversationType: 'direct',
|
||||
fromOrAddedByTrustedContact: false,
|
||||
i18n,
|
||||
hasNickname: false,
|
||||
hasProfileName: true,
|
||||
i18n,
|
||||
isGroupNameVerified: false,
|
||||
isInSystemContacts: false,
|
||||
theme: ThemeType.light,
|
||||
sharedGroupNames: [],
|
||||
@@ -178,10 +178,10 @@ GroupMessageRequest.args = {
|
||||
acceptedMessageRequest: false,
|
||||
};
|
||||
|
||||
export const GroupFromTrustedContact = Template.bind({});
|
||||
GroupFromTrustedContact.args = {
|
||||
export const GroupVerifiedName = Template.bind({});
|
||||
GroupVerifiedName.args = {
|
||||
...groupArgs,
|
||||
fromOrAddedByTrustedContact: true,
|
||||
isGroupNameVerified: true,
|
||||
};
|
||||
|
||||
export function GroupMemberNames(args: Props): React.JSX.Element {
|
||||
|
||||
@@ -22,7 +22,6 @@ import { AxoButton } from '../../axo/AxoButton.dom.tsx';
|
||||
export type Props = {
|
||||
about?: string;
|
||||
acceptedMessageRequest?: boolean;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
groupDescription?: string;
|
||||
hasAvatar?: boolean;
|
||||
hasNickname: boolean;
|
||||
@@ -30,6 +29,7 @@ export type Props = {
|
||||
hasStories?: HasStories;
|
||||
id: string;
|
||||
i18n: LocalizerType;
|
||||
isGroupNameVerified: boolean;
|
||||
isInSystemContacts: boolean;
|
||||
isMe: boolean;
|
||||
invitesCount?: number;
|
||||
@@ -58,13 +58,13 @@ export function ConversationHero({
|
||||
badge,
|
||||
color,
|
||||
conversationType,
|
||||
fromOrAddedByTrustedContact,
|
||||
groupDescription,
|
||||
hasAvatar,
|
||||
hasNickname,
|
||||
hasProfileName,
|
||||
hasStories,
|
||||
id,
|
||||
isGroupNameVerified,
|
||||
isInSystemContacts,
|
||||
isMe,
|
||||
invitesCount,
|
||||
@@ -210,12 +210,11 @@ export function ConversationHero({
|
||||
}
|
||||
|
||||
if (conversationType === 'group') {
|
||||
const nameIsVerified = Boolean(fromOrAddedByTrustedContact);
|
||||
return (
|
||||
<Root>
|
||||
{avatar}
|
||||
<Title title={title} />
|
||||
{!nameIsVerified ? (
|
||||
{!isGroupNameVerified ? (
|
||||
<NameNotVerifiedWarning
|
||||
conversationType={conversationType}
|
||||
onClick={() => toggleProfileNameWarningModal(conversationType)}
|
||||
|
||||
@@ -413,10 +413,11 @@ const renderHeroRow = () => {
|
||||
conversationType="direct"
|
||||
hasNickname={false}
|
||||
hasProfileName
|
||||
id={getDefaultConversation().id}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
id={getDefaultConversation().id}
|
||||
isGroupNameVerified={false}
|
||||
isInSystemContacts={false}
|
||||
isMe={false}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
profileName={getProfileName()}
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
|
||||
@@ -137,6 +137,7 @@ import { toNumber } from './util/toNumber.std.ts';
|
||||
import Actions = Proto.GroupChange.Actions;
|
||||
import AccessRequired = Proto.AccessControl.AccessRequired;
|
||||
import MemberRole = Proto.Member.Role;
|
||||
import { computeGroupNameHash } from './util/Conversation.preload.ts';
|
||||
|
||||
const { compact, difference, flatten, fromPairs, isNumber, omit, values } =
|
||||
lodash;
|
||||
@@ -2030,6 +2031,7 @@ export async function createGroupV2(
|
||||
avatar: avatarAttribute,
|
||||
avatars,
|
||||
groupVersion: 2,
|
||||
groupVerifiedNameHash: computeGroupNameHash(name),
|
||||
masterKey,
|
||||
profileSharing: true,
|
||||
timestamp: now,
|
||||
@@ -5543,6 +5545,13 @@ async function applyGroupChange({
|
||||
const title = actions.modifyTitle.title?.content?.title;
|
||||
if (title != null) {
|
||||
result.name = title.trim();
|
||||
if (ourAci === sourceServiceId) {
|
||||
result.groupVerifiedNameHash = computeGroupNameHash(result.name);
|
||||
// TODO (DESKTOP-10060)
|
||||
window.ConversationController.get(group.id)?.captureChange(
|
||||
'groupVerifiedNameHash'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
`applyGroupChange/${logId}: Clearing group title due to missing data.`
|
||||
@@ -6239,6 +6248,10 @@ async function applyGroupState({
|
||||
return member;
|
||||
});
|
||||
|
||||
if (groupState.version === 0 && sourceServiceId === ourAci && result.name) {
|
||||
result.groupVerifiedNameHash = computeGroupNameHash(result.name);
|
||||
}
|
||||
|
||||
// avatar
|
||||
result = {
|
||||
...result,
|
||||
|
||||
Vendored
+1
@@ -486,6 +486,7 @@ export type ConversationAttributesType = {
|
||||
left?: boolean;
|
||||
groupVersion?: number;
|
||||
storySendMode?: StorySendMode;
|
||||
groupVerifiedNameHash?: string;
|
||||
|
||||
// GroupV1 only
|
||||
members?: Array<string>;
|
||||
|
||||
@@ -627,8 +627,8 @@ export function toGroupV2Record(
|
||||
}
|
||||
|
||||
const avatarColor = conversation.get('colorFromPrimary');
|
||||
|
||||
const masterKey = conversation.get('masterKey');
|
||||
const verifiedNameHash = conversation.get('groupVerifiedNameHash');
|
||||
|
||||
return {
|
||||
masterKey: masterKey != null ? Bytes.fromBase64(masterKey) : null,
|
||||
@@ -646,7 +646,9 @@ export function toGroupV2Record(
|
||||
hideStory: Boolean(conversation.get('hideStory')),
|
||||
avatarColor: avatarColor ?? null,
|
||||
storySendMode,
|
||||
|
||||
verifiedNameHash: verifiedNameHash
|
||||
? Bytes.fromBase64(verifiedNameHash)
|
||||
: null,
|
||||
$unknown: conversationUnknownFieldsToRecord(conversation),
|
||||
};
|
||||
}
|
||||
@@ -1243,9 +1245,19 @@ export async function mergeGroupV2Record(
|
||||
storageID,
|
||||
storageVersion,
|
||||
storySendMode,
|
||||
|
||||
needsStorageServiceSync: false,
|
||||
});
|
||||
|
||||
// We only update verified name hash if it is truthy, to avoid races where a linked
|
||||
// device creates the GroupV2Record (with nullish hash) before the creating device
|
||||
// updates storage service with the verified name hash.
|
||||
if (Bytes.isNotEmpty(groupV2Record.verifiedNameHash)) {
|
||||
conversation.set({
|
||||
groupVerifiedNameHash: Bytes.toBase64(groupV2Record.verifiedNameHash),
|
||||
});
|
||||
}
|
||||
|
||||
conversation.setMuteExpiration(
|
||||
getTimestampFromLong(
|
||||
groupV2Record.mutedUntilTimestamp,
|
||||
|
||||
@@ -255,6 +255,7 @@ import {
|
||||
getPanels,
|
||||
getSelectedConversationId,
|
||||
} from '../selectors/nav.std.ts';
|
||||
import { computeGroupNameHash } from '../../util/Conversation.preload.ts';
|
||||
|
||||
const { chunk, difference, fromPairs, omit, orderBy, pick, values, without } =
|
||||
lodash;
|
||||
@@ -442,6 +443,7 @@ export type ConversationType = ReadonlyDeep<
|
||||
groupVersion?: 1 | 2;
|
||||
groupId?: string;
|
||||
groupLink?: string;
|
||||
groupVerifiedNameHash?: string;
|
||||
terminated?: boolean;
|
||||
acceptedMessageRequest: boolean;
|
||||
secretParams?: string;
|
||||
@@ -4649,6 +4651,13 @@ function updateGroupAttributes(
|
||||
attributes
|
||||
),
|
||||
});
|
||||
if (attributes.title) {
|
||||
conversation.set({
|
||||
groupVerifiedNameHash: computeGroupNameHash(attributes.title),
|
||||
});
|
||||
await DataWriter.updateConversation(conversation.attributes);
|
||||
conversation.captureChange('groupVerifiedNameHash');
|
||||
}
|
||||
onSuccess?.();
|
||||
} catch {
|
||||
onFailure?.();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { PanelType } from '../../types/Panels.std.ts';
|
||||
import { ConversationHero } from '../../components/conversation/ConversationHero.dom.tsx';
|
||||
@@ -14,39 +14,19 @@ import {
|
||||
getPendingAvatarDownloadSelector,
|
||||
} from '../selectors/conversations.dom.ts';
|
||||
import { useSharedGroupNamesOnMount } from '../../util/sharedGroupNames.dom.ts';
|
||||
import {
|
||||
type ConversationType,
|
||||
useConversationsActions,
|
||||
} from '../ducks/conversations.preload.ts';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.ts';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.preload.ts';
|
||||
import { useStoriesActions } from '../ducks/stories.preload.ts';
|
||||
import { getAddedByForGroup } from '../../util/getAddedByForGroup.preload.ts';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships.dom.ts';
|
||||
import { useNavActions } from '../ducks/nav.std.ts';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts.std.ts';
|
||||
import { computeGroupNameHash } from '../../util/Conversation.preload.ts';
|
||||
|
||||
type SmartHeroRowProps = Readonly<{
|
||||
id: string;
|
||||
}>;
|
||||
|
||||
function isFromOrAddedByTrustedContact(
|
||||
conversation: ConversationType
|
||||
): boolean {
|
||||
if (conversation.type === 'direct') {
|
||||
return Boolean(conversation.name) || Boolean(conversation.profileSharing);
|
||||
}
|
||||
|
||||
const addedByConv = getAddedByForGroup(conversation);
|
||||
if (!addedByConv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
addedByConv.isMe || addedByConv.name || addedByConv.profileSharing
|
||||
);
|
||||
}
|
||||
|
||||
export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
id,
|
||||
}: SmartHeroRowProps) {
|
||||
@@ -73,8 +53,6 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
const badge = getPreferredBadge(conversation.badges);
|
||||
const hasStories = hasStoriesSelector(id);
|
||||
const isSignalConversationValue = isSignalConversation(conversation);
|
||||
const fromOrAddedByTrustedContact =
|
||||
isFromOrAddedByTrustedContact(conversation);
|
||||
const { startAvatarDownload } = useConversationsActions();
|
||||
const { pushPanelForConversation } = useNavActions();
|
||||
const { toggleAboutContactModal, toggleProfileNameWarningModal } =
|
||||
@@ -89,6 +67,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
avatarUrl,
|
||||
color,
|
||||
groupDescription,
|
||||
groupVerifiedNameHash,
|
||||
hasAvatar,
|
||||
isMe,
|
||||
membersCount,
|
||||
@@ -96,10 +75,24 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
nicknameFamilyName,
|
||||
profileName,
|
||||
title,
|
||||
titleNoDefault,
|
||||
type,
|
||||
} = conversation;
|
||||
const invitesCount =
|
||||
pendingMemberships.length + pendingApprovalMemberships.length;
|
||||
|
||||
const isGroupNameVerified = useMemo(() => {
|
||||
if (type !== 'group') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!groupVerifiedNameHash || !titleNoDefault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return computeGroupNameHash(titleNoDefault) === groupVerifiedNameHash;
|
||||
}, [groupVerifiedNameHash, titleNoDefault, type]);
|
||||
|
||||
return (
|
||||
<div className={tw('mt-10 flex justify-center')}>
|
||||
<ConversationHero
|
||||
@@ -109,7 +102,6 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
badge={badge}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
fromOrAddedByTrustedContact={fromOrAddedByTrustedContact}
|
||||
groupDescription={groupDescription}
|
||||
hasAvatar={hasAvatar}
|
||||
hasNickname={Boolean(nicknameGivenName || nicknameFamilyName)}
|
||||
@@ -120,6 +112,7 @@ export const SmartHeroRow = memo(function SmartHeroRow({
|
||||
isMe={isMe}
|
||||
invitesCount={invitesCount}
|
||||
isInSystemContacts={isInSystemContacts(conversation)}
|
||||
isGroupNameVerified={isGroupNameVerified}
|
||||
isSignalConversation={isSignalConversationValue}
|
||||
membersCount={membersCount}
|
||||
memberships={memberships}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import { DataReader, DataWriter } from '../sql/Client.preload.ts';
|
||||
import type { ConversationAttributesType } from '../model-types.d.ts';
|
||||
import { maybeDeleteAttachmentFile } from './migrations.preload.ts';
|
||||
import * as Bytes from '../Bytes.std.ts';
|
||||
import { sha256 } from '../Crypto.node.ts';
|
||||
|
||||
async function deleteExternalFiles(
|
||||
conversation: ConversationAttributesType
|
||||
@@ -33,3 +35,7 @@ export async function removeConversation(id: string): Promise<void> {
|
||||
await deleteExternalFiles(existing);
|
||||
}
|
||||
}
|
||||
|
||||
export function computeGroupNameHash(name: string): string {
|
||||
return Bytes.toBase64(sha256(Bytes.fromString(name)));
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
familyName: attributes.nicknameFamilyName ?? attributes.profileFamilyName,
|
||||
firstName: attributes.nicknameGivenName ?? attributes.profileName,
|
||||
groupDescription: attributes.description,
|
||||
groupVerifiedNameHash: attributes.groupVerifiedNameHash,
|
||||
groupVersion,
|
||||
groupId: attributes.groupId,
|
||||
groupLink: buildGroupLink(attributes),
|
||||
|
||||
Reference in New Issue
Block a user