Fix gallery footer author image showing old image (#7910)

* Fix gallery footer author image showing old image

* Fix tests

* Address feedback

* Fix tests and minor refactor
This commit is contained in:
Daniel Espino García 2024-05-06 12:00:19 +02:00 committed by GitHub
parent a6d043670a
commit b98c184e7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 422 additions and 389 deletions

View file

@ -229,7 +229,7 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly =
}
}
export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_PAGE_DEFAULT): Promise<{teams?: Team[]; error?: any}> => {
export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_PAGE_DEFAULT): Promise<{teams?: Team[]; error?: unknown}> => {
try {
const client = NetworkManager.getClient(serverUrl);
const teams = await client.getTeams(page, perPage);
@ -475,3 +475,12 @@ export async function getTeamMembersByIds(serverUrl: string, teamId: string, use
return {error};
}
}
export const buildTeamIconUrl = (serverUrl: string, teamId: string, timestamp = 0) => {
try {
const client = NetworkManager.getClient(serverUrl);
return client.getTeamIconUrl(teamId, timestamp);
} catch (error) {
return '';
}
};

View file

@ -19,7 +19,7 @@ import {getCurrentUser, prepareUsers, queryAllUsers, queryUsersById, queryUsersB
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {getDeviceTimezone} from '@utils/timezone';
import {getUserTimezoneProps, removeUserFromList} from '@utils/user';
import {getLastPictureUpdate, getUserTimezoneProps, removeUserFromList} from '@utils/user';
import {fetchGroupsByNames} from './groups';
import {forceLogoutIfNecessary} from './session';
@ -796,6 +796,11 @@ export const buildProfileImageUrl = (serverUrl: string, userId: string, timestam
}
};
export const buildProfileImageUrlFromUser = (serverUrl: string, user: UserModel | UserProfile) => {
const lastPictureUpdate = getLastPictureUpdate(user);
return buildProfileImageUrl(serverUrl, user.id, lastPictureUpdate);
};
export const autoUpdateTimezone = async (serverUrl: string) => {
let database;
try {

View file

@ -6,16 +6,15 @@ import {useIntl} from 'react-intl';
import {Keyboard, Platform, StyleSheet, TouchableOpacity, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
import {Screens, View as ViewConstant} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import NetworkManager from '@managers/network_manager';
import {openAsBottomSheet} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import type {Client} from '@client/rest';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
@ -37,12 +36,6 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
const intl = useIntl();
const theme = useTheme();
const serverUrl = useServerUrl();
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
// do nothing, client is not set
}
const fromWebHook = post.props?.from_webhook === 'true';
const iconOverride = enablePostIconOverride && post.props?.use_user_icon !== 'true';
@ -51,7 +44,7 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
const frameSize = ViewConstant.PROFILE_PICTURE_SIZE;
const pictureSize = isEmoji ? ViewConstant.PROFILE_PICTURE_EMOJI_SIZE : ViewConstant.PROFILE_PICTURE_SIZE;
const borderRadius = isEmoji ? 0 : ViewConstant.PROFILE_PICTURE_SIZE / 2;
const overrideIconUrl = client?.getAbsoluteUrl(post.props?.override_icon_url);
const overrideIconUrl = buildAbsoluteUrl(serverUrl, post.props?.override_icon_url);
let iconComponent: ReactNode;
if (overrideIconUrl) {

View file

@ -6,14 +6,14 @@ import {Image as RNImage} from 'react-native';
import FastImage, {type Source} from 'react-native-fast-image';
import Animated from 'react-native-reanimated';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import NetworkManager from '@managers/network_manager';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
@ -49,6 +49,15 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
width: size,
}), [size]);
const imgSource = useMemo(() => {
if (!author || typeof source === 'string') {
return undefined;
}
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, author);
return source ?? {uri: buildAbsoluteUrl(serverUrl, pictureUrl)};
}, [author, serverUrl, source]);
if (typeof source === 'string') {
return (
<CompassIcon
@ -59,38 +68,21 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
);
}
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
// handle below that the client is not set
if (imgSource?.uri?.startsWith('file://')) {
return (
<AnimatedImage
key={imgSource.uri}
ref={forwardRef}
style={fIStyle}
source={{uri: imgSource.uri}}
/>
);
}
if (author && client) {
let lastPictureUpdate = 0;
const isBot = ('isBot' in author) ? author.isBot : author.is_bot;
if (isBot) {
lastPictureUpdate = ('isBot' in author) ? author.props?.bot_last_icon_update : author.bot_last_icon_update || 0;
} else {
lastPictureUpdate = ('lastPictureUpdate' in author) ? author.lastPictureUpdate : author.last_picture_update || 0;
}
const pictureUrl = client.getProfilePictureUrl(author.id, lastPictureUpdate);
const imgSource = source ?? {uri: `${serverUrl}${pictureUrl}`};
if (imgSource.uri?.startsWith('file://')) {
return (
<AnimatedImage
key={pictureUrl}
ref={forwardRef}
style={fIStyle}
source={{uri: imgSource.uri}}
/>
);
}
if (imgSource) {
return (
<AnimatedFastImage
key={pictureUrl}
key={imgSource.uri}
// @ts-expect-error TS expects old type ref
ref={forwardRef}
@ -99,6 +91,7 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
/>
);
}
return (
<CompassIcon
name={ACCOUNT_OUTLINE_IMAGE}

View file

@ -5,6 +5,8 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View, Text, type StyleProp, type TextStyle} from 'react-native';
import FastImage from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildTeamIconUrl} from '@actions/remote/team';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import NetworkManager from '@managers/network_manager';
@ -124,7 +126,7 @@ export default function TeamIcon({
teamIconContent = (
<FastImage
style={styles.image}
source={{uri: `${serverUrl}${client!.getTeamIconUrl(id, lastIconUpdate)}`}}
source={{uri: buildAbsoluteUrl(serverUrl, buildTeamIconUrl(serverUrl, id, lastIconUpdate))}}
onError={handleImageError}
/>
);

View file

@ -9,43 +9,7 @@ exports[`components/channel_list_row should show no results 1`] = `
"flexGrow": 1,
}
}
data={
[
{
"data": [
{
"auth_service": "",
"create_at": 1111,
"delete_at": 0,
"email": "john@doe.com",
"first_name": "",
"id": "1",
"last_name": "",
"locale": "",
"nickname": "",
"notify_props": {
"channel": "true",
"comments": "never",
"desktop": "mention",
"desktop_sound": "true",
"email": "true",
"first_name": "true",
"highlight_keys": "",
"mention_keys": "",
"push": "mention",
"push_status": "away",
},
"position": "",
"roles": "",
"update_at": 1111,
"username": "johndoe",
},
],
"first": true,
"id": "J",
},
]
}
data={[]}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={15}
@ -54,7 +18,6 @@ exports[`components/channel_list_row should show no results 1`] = `
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onContentSizeChange={[Function]}
onEndReached={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
@ -71,201 +34,194 @@ exports[`components/channel_list_row should show no results 1`] = `
"flex": 1,
}
}
testID="UserListRow.section_list"
testID="UserListRow.flat_list"
viewabilityConfigCallbackPairs={[]}
>
<View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
style={
[
{
"alignItems": "center",
"flexGrow": 1,
"justifyContent": "center",
},
{
"paddingBottom": 0,
},
]
}
>
<View
style={
{
"backgroundColor": "#ffffff",
"alignItems": "center",
"flexGrow": 1,
"height": "100%",
"justifyContent": "center",
"paddingHorizontal": 32,
}
}
>
<View
style={
{
"backgroundColor": "rgba(63,67,80,0.08)",
"height": 24,
"justifyContent": "center",
"paddingLeft": 16,
}
}
>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 12,
"fontWeight": "600",
"lineHeight": 16,
}
}
>
J
</Text>
</View>
</View>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
{
"opacity": 1,
}
}
>
<View
<RNSVGSvgView
align="xMidYMid"
bbHeight={149}
bbWidth={140}
fill="none"
focusable={false}
height={149}
meetOrSlice={0}
minX={0}
minY={0}
style={
[
[
{
"alignItems": "center",
"flexDirection": "row",
"height": 40,
"paddingBottom": 8,
"paddingTop": 4,
},
{
"opacity": 1,
"paddingHorizontal": 20,
},
undefined,
],
undefined,
{
"backgroundColor": "transparent",
"borderWidth": 0,
},
{
"flex": 0,
"height": 149,
"width": 140,
},
]
}
testID="create_direct_message.user_list.user_item.1.1"
vbHeight={149}
vbWidth={140}
width={140}
>
<View
style={
<RNSVGGroup
fill={null}
propList={
[
{
"alignItems": "center",
"justifyContent": "center",
},
{
"height": 24,
"width": 24,
},
{
"marginRight": 12,
},
"fill",
]
}
testID="create_direct_message.user_list.user_item.1.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
style={
<RNSVGEllipse
cx={70}
cy={122.5}
fill={
{
"color": "rgba(63,67,80,0.48)",
"payload": 4278190080,
"type": 0,
}
}
fillOpacity={0.06}
propList={
[
"fill",
"fillOpacity",
]
}
rx={45}
ry={3}
/>
<RNSVGPath
d="M37.593 38.008c4.754-4.815 10.754-7.295 17.989-7.428 7.101.133 13.065 2.601 17.892 7.428 4.815 4.827 7.295 10.791 7.428 17.892-.133 7.235-2.601 13.223-7.428 17.99-4.827 4.754-10.791 7.27-17.892 7.512-7.235-.254-13.223-2.758-17.99-7.513-4.754-4.766-7.258-10.766-7.512-18 .254-7.102 2.758-13.066 7.513-17.881z"
fill={
{
"payload": 4294967295,
"type": 0,
}
}
opacity={0.4}
propList={
[
"fill",
]
}
/>
</View>
<View
style={
{
"flex": 1,
}
}
>
<View
style={
<RNSVGPath
d="M78.887 51.382c-2.152-6.992-6.225-12.225-12.226-15.69-6.001-3.465-12.57-4.376-19.701-2.744-3.9.996-7.297 2.718-10.22 5.163 3.269-3.567 7.415-6.037 12.428-7.416 7.13-1.633 13.732-.703 19.787 2.793s10.16 8.748 12.313 15.74c1.322 5.037 1.256 9.862-.21 14.47-1.455 4.614-4.067 8.49-7.84 11.611 2.833-3.087 4.783-6.713 5.844-10.894 1.05-4.187.991-8.523-.175-13.033z"
fill={
{
"flex": 1,
"flexDirection": "row",
"payload": 4278190080,
"type": 0,
}
}
>
<Text
numberOfLines={1}
style={
{
"color": "#3f4350",
"flex": 0,
"flexShrink": 1,
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
}
fillOpacity={0.4}
propList={
[
"fill",
"fillOpacity",
]
}
/>
<RNSVGPath
d="M86.76 53.929c-.508-7.506-3.554-14.097-9.126-19.774-6.345-6.05-13.67-9.08-21.973-9.08-8.303 0-15.616 3.03-21.961 9.08-6.08 6.315-9.126 13.591-9.126 21.855 0 8.262 3.046 15.551 9.126 21.854 5.825 5.556 12.485 8.551 19.967 8.984 7.481.445 14.383-1.611 20.728-6.146l4.75 4.727 6.08-6.05-4.75-4.727c4.69-6.302 6.78-13.218 6.285-20.723zm-13.126 19.87c-4.823 4.726-10.782 7.228-17.876 7.468-7.228-.252-13.211-2.742-17.973-7.469-4.75-4.727-7.252-10.692-7.506-17.885.254-7.06 2.756-12.99 7.506-17.789 4.75-4.787 10.745-7.252 17.973-7.385 7.094.133 13.053 2.586 17.876 7.385 4.81 4.8 7.288 10.73 7.42 17.79-.132 7.192-2.598 13.157-7.42 17.884z"
fill={
{
"payload": 4290428617,
"type": 0,
}
testID="create_direct_message.user_list.user_item.1.1.display_name"
>
johndoe
</Text>
<View
style={
{
"flex": 1,
}
}
propList={
[
"fill",
]
}
/>
<RNSVGPath
d="M106.202 114.187c-1.568.448-2.728.291-3.482-.472L78.06 86.651c-.754-.763-1.065-1.743-.945-2.954s.873-2.567 2.261-4.093c1.508-1.393 2.848-2.192 4.044-2.386 1.197-.193 2.166.158 2.92 1.054l26.921 24.957c.754.763.874 1.901.371 3.427-.502 1.525-1.448 3.051-2.824 4.577-1.495 1.526-3.039 2.506-4.606 2.954z"
fill={
{
"payload": 4294949919,
"type": 0,
}
/>
<View
style={
{
"alignItems": "center",
"justifyContent": "center",
"marginLeft": 12,
}
}
propList={
[
"fill",
]
}
/>
<RNSVGPath
d="M108.007 98.343l-10.08 10.164-12.155-13.34 8.915-9.106 13.32 12.281z"
fill={
{
"payload": 4286207488,
"type": 0,
}
>
<Icon
color="rgba(63,67,80,0.32)"
name="circle-outline"
size={28}
/>
</View>
</View>
</View>
</View>
}
propList={
[
"fill",
]
}
/>
</RNSVGGroup>
</RNSVGSvgView>
<Text
style={
{
"color": "#3f4350",
"fontFamily": "Metropolis-SemiBold",
"fontSize": 20,
"fontWeight": "600",
"lineHeight": 28,
"textAlign": "center",
}
}
>
No matches found for “some term”
</Text>
<Text
style={
{
"color": "rgba(63,67,80,0.72)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
"marginTop": 8,
}
}
>
Check the spelling or try another search.
</Text>
</View>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
/>
<View
onLayout={[Function]}
>
@ -309,6 +265,7 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
"first_name": "",
"id": "1",
"last_name": "",
"last_picture_update": 123456,
"locale": "",
"nickname": "",
"notify_props": {
@ -480,15 +437,40 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
}
testID="create_direct_message.user_list.user_item.1.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
<View
style={
{
"color": "rgba(63,67,80,0.48)",
}
[
{
"overflow": "hidden",
},
{
"backgroundColor": "#ffffff",
"borderRadius": 12,
"height": 24,
"width": 24,
},
]
}
/>
>
<FastImageView
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "https://community.mattermost.com/api/v4/users/1/image?_=123456",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<View
style={
@ -597,6 +579,7 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
"first_name": "",
"id": "1",
"last_name": "",
"last_picture_update": 123456,
"locale": "",
"nickname": "",
"notify_props": {
@ -768,15 +751,40 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
}
testID="create_direct_message.user_list.user_item.1.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
<View
style={
{
"color": "rgba(63,67,80,0.48)",
}
[
{
"overflow": "hidden",
},
{
"backgroundColor": "#ffffff",
"borderRadius": 12,
"height": 24,
"width": 24,
},
]
}
/>
>
<FastImageView
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "https://community.mattermost.com/api/v4/users/1/image?_=123456",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<View
style={
@ -885,6 +893,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
"first_name": "",
"id": "1",
"last_name": "",
"last_picture_update": 123456,
"locale": "",
"nickname": "",
"notify_props": {
@ -918,6 +927,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
"first_name": "",
"id": "2",
"last_name": "",
"last_picture_update": 123456,
"locale": "",
"nickname": "",
"notify_props": {
@ -1089,15 +1099,40 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
}
testID="create_direct_message.user_list.user_item.1.1.profile_picture"
>
<Icon
name="account-outline"
size={24}
<View
style={
{
"color": "rgba(63,67,80,0.48)",
}
[
{
"overflow": "hidden",
},
{
"backgroundColor": "#ffffff",
"borderRadius": 12,
"height": 24,
"width": 24,
},
]
}
/>
>
<FastImageView
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "https://community.mattermost.com/api/v4/users/1/image?_=123456",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<View
style={
@ -1281,15 +1316,40 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
}
testID="create_direct_message.user_list.user_item.2.2.profile_picture"
>
<Icon
name="account-outline"
size={24}
<View
style={
{
"color": "rgba(63,67,80,0.48)",
}
[
{
"overflow": "hidden",
},
{
"backgroundColor": "#ffffff",
"borderRadius": 12,
"height": 24,
"width": 24,
},
]
}
/>
>
<FastImageView
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "https://community.mattermost.com/api/v4/users/2/image?_=123456",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<View
style={

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Image} from 'react-native';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
@ -10,6 +11,18 @@ import UserList from '.';
import type Database from '@nozbe/watermelondb/Database';
const mockClient = TestHelper.createClient();
jest.mock('@managers/network_manager', () => {
const original = jest.requireActual('@managers/network_manager');
return {
...original,
getClient: () => {
return mockClient;
},
};
});
describe('components/channel_list_row', () => {
let database: Database;
const user: UserProfile = {
@ -26,6 +39,7 @@ describe('components/channel_list_row', () => {
position: '',
roles: '',
locale: '',
last_picture_update: 123456,
notify_props: {
channel: 'true',
comments: 'never',
@ -54,6 +68,7 @@ describe('components/channel_list_row', () => {
position: '',
roles: '',
locale: '',
last_picture_update: 123456,
notify_props: {
channel: 'true',
comments: 'never',
@ -68,15 +83,26 @@ describe('components/channel_list_row', () => {
},
};
const originalResolveAssetSource = Image.resolveAssetSource;
beforeAll(async () => {
const server = await TestHelper.setupServerDatabase();
database = server.database;
// This is needed to properly populate the URLs until
// https://github.com/facebook/react-native/pull/43497
// gets into React Native Jest code.
Image.resolveAssetSource = jest.fn().mockImplementation((source) => source);
});
afterAll(() => {
Image.resolveAssetSource = originalResolveAssetSource;
});
it('should show no results', () => {
const wrapper = renderWithEverything(
<UserList
profiles={[user]}
profiles={[]}
testID='UserListRow'
currentUserId={'1'}
handleSelectProfile={() => {
@ -87,6 +113,7 @@ describe('components/channel_list_row', () => {
}}
loading={true}
selectedIds={{}}
term={'some term'}
showNoResults={true}
tutorialWatched={true}
/>,

View file

@ -6,11 +6,11 @@ import React from 'react';
import {View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import {useServerUrl} from '@context/server';
import NetworkManager from '@managers/network_manager';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
@ -37,23 +37,15 @@ const Group = ({theme, users}: Props) => {
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
return null;
}
const rows = chunk(users, 5);
const groups = rows.map((c, k) => {
const group = c.map((u, i) => {
const pictureUrl = client!.getProfilePictureUrl(u.id, u.lastPictureUpdate);
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, u);
return (
<FastImage
key={pictureUrl + i.toString()}
style={[styles.profile, {transform: [{translateX: -(i * 24)}]}]}
source={{uri: `${serverUrl}${pictureUrl}`}}
source={{uri: buildAbsoluteUrl(serverUrl, pictureUrl)}}
/>
);
});

View file

@ -5,12 +5,12 @@ import React from 'react';
import {View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import NetworkManager from '@managers/network_manager';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
@ -37,21 +37,13 @@ const GroupAvatars = ({users}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
return null;
}
const group = users.map((u, i) => {
const pictureUrl = client!.getProfilePictureUrl(u.id, u.lastPictureUpdate);
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, u);
return (
<FastImage
key={pictureUrl + i.toString()}
style={[styles.profile, {transform: [{translateX: -(i * 12)}]}]}
source={{uri: `${serverUrl}${pictureUrl}`}}
source={{uri: buildAbsoluteUrl(serverUrl, pictureUrl)}}
/>
);
});

View file

@ -4,17 +4,16 @@
import React, {useCallback, useMemo, useState} from 'react';
import {Platform, View} from 'react-native';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import ProfileImage from '@components/profile_picture';
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useDidUpdate from '@hooks/did_update';
import NetworkManager from '@managers/network_manager';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import ProfileImagePicker from './profile_image_picker';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
type ChangeProfilePictureProps = {
@ -47,24 +46,18 @@ const EditProfilePicture = ({user, onUpdateProfilePicture}: ChangeProfilePicture
const serverUrl = useServerUrl();
const theme = useTheme();
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
// does nothing
}
const [pictureUrl, setPictureUrl] = useState<string|undefined>(client?.getProfilePictureUrl(user.id, user.lastPictureUpdate));
const [pictureUrl, setPictureUrl] = useState<string|undefined>(() => {
return buildProfileImageUrlFromUser(serverUrl, user);
});
const styles = getStyleSheet(theme);
useDidUpdate(() => {
const url = user.id && client ? client.getProfilePictureUrl(user.id, user.lastPictureUpdate) : undefined;
const url = user.id ? buildProfileImageUrlFromUser(serverUrl, user) : undefined;
if (url !== pictureUrl) {
setPictureUrl(url);
}
}, [user.id, user.lastPictureUpdate]);
}, [user]);
const handleProfileImage = useCallback((images?: FileInfo[]) => {
let isRemoved = true;

View file

@ -6,13 +6,13 @@ import {useIntl} from 'react-intl';
import {TouchableOpacity} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import NetworkManager from '@managers/network_manager';
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
import PanelItem from '@screens/edit_profile/components/panel_item';
import {bottomSheet} from '@screens/navigation';
@ -22,7 +22,6 @@ import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
const hitSlop = {top: 100, bottom: 20, right: 20, left: 100};
@ -57,21 +56,9 @@ type ImagePickerProps = {
};
const hasPictureUrl = (user: UserModel, serverUrl: string) => {
const {id, lastPictureUpdate} = user;
let client: Client | undefined;
let profileImageUrl: string | undefined;
try {
client = NetworkManager.getClient(serverUrl);
profileImageUrl = client.getProfilePictureUrl(id, lastPictureUpdate);
} catch {
return false;
}
// Check if image url includes query string for timestamp. If so,
// it means the image has been updated from the default, i.e. '.../image?_=1544159746868'
return Boolean(profileImageUrl?.includes('image?_'));
return buildProfileImageUrlFromUser(serverUrl, user).includes('image?_');
};
const ProfileImagePicker = ({

View file

@ -1,18 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React from 'react';
import {StyleSheet, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrl} from '@actions/remote/user';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import {useServerUrl} from '@app/context/server';
import CompassIcon from '@components/compass_icon';
import {useServerUrl} from '@context/server';
import {changeOpacity} from '@utils/theme';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
authorId?: string;
author?: UserModel;
overrideIconUrl?: string;
}
@ -32,28 +34,22 @@ const styles = StyleSheet.create({
},
});
const Avatar = ({authorId, overrideIconUrl}: Props) => {
const Avatar = ({
author,
overrideIconUrl,
}: Props) => {
const serverUrl = useServerUrl();
const avatarUri = useMemo(() => {
try {
if (overrideIconUrl) {
return buildAbsoluteUrl(serverUrl, overrideIconUrl);
} else if (authorId) {
const pictureUrl = buildProfileImageUrl(serverUrl, authorId);
return `${serverUrl}${pictureUrl}`;
}
return undefined;
} catch {
return undefined;
}
}, [serverUrl, authorId, overrideIconUrl]);
let uri = overrideIconUrl;
if (!uri && author) {
uri = buildProfileImageUrlFromUser(serverUrl, author);
}
let picture;
if (avatarUri) {
if (uri) {
picture = (
<FastImage
source={{uri: avatarUri}}
source={{uri: buildAbsoluteUrl(serverUrl, uri)}}
style={[styles.avatar, styles.avatarRadius]}
/>
);

View file

@ -125,10 +125,10 @@ const Footer = ({
<View style={styles.container}>
<View style={styles.details}>
{item.type !== 'avatar' &&
<Avatar
authorId={author?.id}
overrideIconUrl={overrideIconUrl}
/>
<Avatar
author={author}
overrideIconUrl={overrideIconUrl}
/>
}
<Details
channelName={item.type === 'avatar' ? '' : channelName}

View file

@ -6,12 +6,12 @@ import React from 'react';
import {Image, StyleSheet, View} from 'react-native';
import FastImage, {type Source} from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import NetworkManager from '@managers/network_manager';
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
import type {Client} from '@client/rest';
import type {WithDatabaseArgs} from '@typings/database/database';
import type UserModel from '@typings/database/models/servers/user';
@ -36,17 +36,10 @@ const styles = StyleSheet.create({
});
const NotificationIcon = ({author, enablePostIconOverride, fromWebhook, overrideIconUrl, serverUrl, useUserIcon}: NotificationIconProps) => {
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
// do nothing, client is not set
}
let icon;
if (client && fromWebhook && !useUserIcon && enablePostIconOverride) {
if (fromWebhook && !useUserIcon && enablePostIconOverride) {
if (overrideIconUrl) {
const source: Source = {uri: client.getAbsoluteUrl(overrideIconUrl)};
const source: Source = {uri: buildAbsoluteUrl(serverUrl, overrideIconUrl)};
icon = (
<FastImage
source={source}
@ -61,13 +54,13 @@ const NotificationIcon = ({author, enablePostIconOverride, fromWebhook, override
/>
);
}
} else if (author && client) {
const pictureUrl = client.getProfilePictureUrl(author.id, author.lastPictureUpdate);
} else if (author) {
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, author);
icon = (
<FastImage
key={pictureUrl}
style={{width: IMAGE_SIZE, height: IMAGE_SIZE, borderRadius: (IMAGE_SIZE / 2)}}
source={{uri: `${serverUrl}${pictureUrl}`}}
source={{uri: buildAbsoluteUrl(serverUrl, pictureUrl)}}
/>
);
} else {

View file

@ -5,12 +5,13 @@ import {useIntl} from 'react-intl';
import {Text, TouchableOpacity, View} from 'react-native';
import Animated from 'react-native-reanimated';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import {GalleryInit} from '@context/gallery';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {useGalleryItem} from '@hooks/gallery';
import NetworkManager from '@managers/network_manager';
import {openGalleryAtIndex} from '@utils/gallery';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -93,14 +94,8 @@ const UserProfileTitle = ({
if (enablePostIconOverride && userIconOverride) {
imageUrl = userIconOverride;
} else {
try {
const client = NetworkManager.getClient(serverUrl);
const lastPictureUpdate = user.isBot ? (user.props?.bot_last_icon_update || 0) : user.lastPictureUpdate;
const pictureUrl = client.getProfilePictureUrl(user.id, lastPictureUpdate);
imageUrl = `${serverUrl}${pictureUrl}`;
} catch {
// handle below that the client is not set
}
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, user);
imageUrl = buildAbsoluteUrl(serverUrl, pictureUrl);
}
if (imageUrl) {

View file

@ -380,3 +380,11 @@ export const getEmailIntervalTexts = (interval: string) => {
};
return intervalTexts[interval];
};
export const getLastPictureUpdate = (user: UserModel | UserProfile) => {
if ('isBot' in user) {
return user.isBot ? user.props?.bot_last_icon_update : user.lastPictureUpdate || 0;
}
return user.is_bot ? user.bot_last_icon_update : user.last_picture_update || 0;
};

View file

@ -5,9 +5,10 @@ import React from 'react';
import {View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {buildAbsoluteUrl} from '@actions/remote/file';
import {buildProfileImageUrlFromUser} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
import NetworkManager from '@managers/network_manager';
import {useShareExtensionServerUrl} from '@share/state';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -34,7 +35,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const Avatar = ({author, theme}: Props) => {
const serverUrl = useShareExtensionServerUrl();
const style = getStyleSheet(theme);
const isBot = author?.isBot || false;
let pictureUrl = '';
if (author?.deleteAt) {
@ -48,24 +48,12 @@ const Avatar = ({author, theme}: Props) => {
}
if (author && serverUrl) {
try {
const client = NetworkManager.getClient(serverUrl);
let lastPictureUpdate = 0;
if (isBot) {
lastPictureUpdate = author?.props?.bot_last_icon_update || 0;
} else {
lastPictureUpdate = author?.lastPictureUpdate || 0;
}
pictureUrl = client.getProfilePictureUrl(author.id, lastPictureUpdate);
} catch {
// handle below that the client is not set
}
pictureUrl = buildProfileImageUrlFromUser(serverUrl, author);
}
let icon;
if (pictureUrl) {
const imgSource = {uri: `${serverUrl}${pictureUrl}`};
if (pictureUrl && serverUrl) {
const imgSource = {uri: buildAbsoluteUrl(serverUrl, pictureUrl)};
icon = (
<FastImage
key={pictureUrl}