From 9699318043f54949e166b06faa8cc9f48da10a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Vay=C3=A1?= Date: Fri, 13 Feb 2026 12:49:07 +0100 Subject: [PATCH] Show profile message actions for non-DM user profiles (#9496) * Show profile message actions for non-DM user profiles * Add DM profile visibility safeguards and coverage * Simplify DM channel name parsing and add reversed order test Remove redundant manual split('__') validation in isDirectMessageWithViewedUser, relying on getUserIdFromChannelName which already handles both name orderings. Add test for reversed DM channel name order (B__A) to prevent regressions. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- app/screens/user_profile/index.test.ts | 73 +++++++++++++++++++ app/screens/user_profile/index.ts | 24 ++++-- .../user_profile/user_profile.test.tsx | 61 ++++++++++++++++ app/screens/user_profile/user_profile.tsx | 29 +++++++- 4 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 app/screens/user_profile/index.test.ts create mode 100644 app/screens/user_profile/user_profile.test.tsx diff --git a/app/screens/user_profile/index.test.ts b/app/screens/user_profile/index.test.ts new file mode 100644 index 000000000..a1a5a7bdd --- /dev/null +++ b/app/screens/user_profile/index.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {General} from '@constants'; + +import {isDirectMessageWithViewedUser} from './index'; + +describe('screens/user_profile/index', () => { + it('should return true when viewing the DM counterpart user', () => { + const result = isDirectMessageWithViewedUser( + { + name: 'current_user_id__other_user_id', + type: General.DM_CHANNEL, + }, + 'current_user_id', + 'other_user_id', + ); + + expect(result).toBe(true); + }); + + it('should return false when viewing a user different from the DM counterpart', () => { + const result = isDirectMessageWithViewedUser( + { + name: 'current_user_id__other_user_id', + type: General.DM_CHANNEL, + }, + 'current_user_id', + 'someone_else', + ); + + expect(result).toBe(false); + }); + + it('should return false for non-DM channels', () => { + const result = isDirectMessageWithViewedUser( + { + name: 'current_user_id__other_user_id', + type: General.OPEN_CHANNEL, + }, + 'current_user_id', + 'other_user_id', + ); + + expect(result).toBe(false); + }); + + it('should return true when DM channel name has reversed user order', () => { + const result = isDirectMessageWithViewedUser( + { + name: 'other_user_id__current_user_id', + type: General.DM_CHANNEL, + }, + 'current_user_id', + 'other_user_id', + ); + + expect(result).toBe(true); + }); + + it('should return false when DM channel name is malformed', () => { + const result = isDirectMessageWithViewedUser( + { + name: 'malformed_channel_name', + type: General.DM_CHANNEL, + }, + 'current_user_id', + 'other_user_id', + ); + + expect(result).toBe(false); + }); +}); diff --git a/app/screens/user_profile/index.ts b/app/screens/user_profile/index.ts index 8abfc0d62..dc4156513 100644 --- a/app/screens/user_profile/index.ts +++ b/app/screens/user_profile/index.ts @@ -14,7 +14,7 @@ import {observeCanManageChannelMembers, observePermissionForChannel} from '@quer import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system'; import {observeTeammateNameDisplay, observeCurrentUser, observeUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user'; import {isDefaultChannel} from '@utils/channel'; -import {isSystemAdmin, sortCustomProfileAttributes, convertToAttributesMap, convertProfileAttributesToCustomAttributes} from '@utils/user'; +import {isSystemAdmin, sortCustomProfileAttributes, convertToAttributesMap, convertProfileAttributesToCustomAttributes, getUserIdFromChannelName} from '@utils/user'; import UserProfile from './user_profile'; @@ -26,6 +26,20 @@ type EnhancedProps = WithDatabaseArgs & { channelId?: string; } +type DMChannelInfo = { + name: string; + type: string; +}; + +export function isDirectMessageWithViewedUser(channel: DMChannelInfo|undefined, currentUserId: string, viewedUserId: string|undefined): boolean { + if (!channel || channel.type !== General.DM_CHANNEL || !viewedUserId) { + return false; + } + + const dmUserId = getUserIdFromChannelName(currentUserId, channel.name); + return Boolean(dmUserId) && dmUserId === viewedUserId; +} + const enhanced = withObservables([], ({channelId, database, userId}: EnhancedProps) => { const currentUser = observeCurrentUser(database); const currentUserId = observeCurrentUserId(database); @@ -36,9 +50,9 @@ const enhanced = withObservables([], ({channelId, database, userId}: EnhancedPro const isDC = channel ? channel.pipe( switchMap((c) => of$(isDefaultChannel(c))), ) : of$(false); - const isDirectMessage = channelId ? channel.pipe( - switchMap((c) => of$(c?.type === General.DM_CHANNEL)), - ) : of$(false); + const isDirectMessageWithUser = combineLatest([channel, currentUserId, user]).pipe( + map(([c, currentId, viewedUser]) => isDirectMessageWithViewedUser(c, currentId, viewedUser?.id)), + ); const teamId = channel.pipe(switchMap((c) => (c?.teamId ? of$(c.teamId) : observeCurrentTeamId(database)))); const isTeamAdmin = teamId.pipe(switchMap((id) => observeUserIsTeamAdmin(database, userId, id))); const systemAdmin = user.pipe(switchMap((u) => of$(u?.roles ? isSystemAdmin(u.roles) : false))); @@ -81,7 +95,7 @@ const enhanced = withObservables([], ({channelId, database, userId}: EnhancedPro isChannelAdmin, isCustomStatusEnabled, isDefaultChannel: isDC, - isDirectMessage, + isDirectMessageWithUser, isMilitaryTime, isSystemAdmin: systemAdmin, isTeamAdmin, diff --git a/app/screens/user_profile/user_profile.test.tsx b/app/screens/user_profile/user_profile.test.tsx new file mode 100644 index 000000000..5e50d0297 --- /dev/null +++ b/app/screens/user_profile/user_profile.test.tsx @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {shouldShowUserProfileOptions} from './user_profile'; + +describe('screens/user_profile/user_profile', () => { + it('should hide profile options when viewing the same user in a DM from channel context', () => { + const shouldShow = shouldShowUserProfileOptions({ + channelContext: true, + isDirectMessageWithUser: true, + manageMode: false, + override: false, + }); + + expect(shouldShow).toBe(false); + }); + + it('should show profile options when viewing another user in a DM from channel context', () => { + const shouldShow = shouldShowUserProfileOptions({ + channelContext: true, + isDirectMessageWithUser: false, + manageMode: false, + override: false, + }); + + expect(shouldShow).toBe(true); + }); + + it('should hide profile options when in manage mode', () => { + const shouldShow = shouldShowUserProfileOptions({ + channelContext: true, + isDirectMessageWithUser: false, + manageMode: true, + override: false, + }); + + expect(shouldShow).toBe(false); + }); + + it('should hide profile options when override is enabled', () => { + const shouldShow = shouldShowUserProfileOptions({ + channelContext: false, + isDirectMessageWithUser: false, + manageMode: false, + override: true, + }); + + expect(shouldShow).toBe(false); + }); + + it('should show profile options outside channel context even for direct message with viewed user', () => { + const shouldShow = shouldShowUserProfileOptions({ + channelContext: false, + isDirectMessageWithUser: true, + manageMode: false, + override: false, + }); + + expect(shouldShow).toBe(true); + }); +}); diff --git a/app/screens/user_profile/user_profile.tsx b/app/screens/user_profile/user_profile.tsx index 032dae7a3..44adac7f2 100644 --- a/app/screens/user_profile/user_profile.tsx +++ b/app/screens/user_profile/user_profile.tsx @@ -34,7 +34,7 @@ type Props = { isChannelAdmin: boolean; canManageAndRemoveMembers?: boolean; isCustomStatusEnabled: boolean; - isDirectMessage: boolean; + isDirectMessageWithUser: boolean; isDefaultChannel: boolean; isMilitaryTime: boolean; isSystemAdmin: boolean; @@ -66,6 +66,22 @@ const messages = defineMessages({ }); const channelContextScreens: AvailableScreens[] = [Screens.CHANNEL, Screens.THREAD]; +type ShouldShowUserProfileOptionsProps = { + channelContext: boolean; + isDirectMessageWithUser: boolean; + manageMode: boolean; + override: boolean; +}; + +export function shouldShowUserProfileOptions({ + channelContext, + isDirectMessageWithUser, + manageMode, + override, +}: ShouldShowUserProfileOptionsProps) { + return (!isDirectMessageWithUser || !channelContext) && !override && !manageMode; +} + const UserProfile = ({ canChangeMemberRoles, canManageAndRemoveMembers, @@ -77,7 +93,7 @@ const UserProfile = ({ isChannelAdmin, isCustomStatusEnabled, isDefaultChannel, - isDirectMessage, + isDirectMessageWithUser, isMilitaryTime, isSystemAdmin, isTeamAdmin, @@ -113,7 +129,12 @@ const UserProfile = ({ } const showCustomStatus = isCustomStatusEnabled && Boolean(customStatus) && !user.isBot && !isCustomStatusExpired(user); - const showUserProfileOptions = (!isDirectMessage || !channelContext) && !override && !manageMode; + const showUserProfileOptions = shouldShowUserProfileOptions({ + channelContext, + isDirectMessageWithUser, + manageMode, + override, + }); const showNickname = Boolean(user.nickname) && !override && !user.isBot && !manageMode; const showPosition = Boolean(user.position) && !override && !user.isBot && !manageMode; const showLocalTime = Boolean(localTime) && !override && !user.isBot && !manageMode; @@ -175,7 +196,7 @@ const UserProfile = ({ if (currentUserId !== user.id) { fetchTeamAndChannelMembership(serverUrl, user.id, teamId, channelId); } - }, []); + }, [channelId, currentUserId, serverUrl, teamId, user.id]); const renderContent = () => { return (