MM-40203: permalink modal viewer for mentions (#5999)
* Permalink initial commit * Fixes: 10 items per page * MM-40203: permalink modal viewer for mentions Is triggered by pressing on an item in mentions, and shows posts around that item, including the item in a modal. * Adds previously deleted file * address feedback * Move showPermalink as a remote action * address more feedback * fetchPostsAround to only return PostModel * Attempt to autoscroll to highlighted item * Permalink to not highlight saved and pinned posts * Add bottom margin using insets to permalink screen * Use lottie loading indicator * Switch to channel * Missing translation string Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
a43dad53e1
commit
5178091ab0
16 changed files with 456 additions and 45 deletions
|
|
@ -4,7 +4,7 @@
|
|||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {showPermalink} from '@actions/local/permalink';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DeepLinkTypes from '@constants/deep_linking';
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {queryTeamById, queryTeamByName} from '@queries/servers/team';
|
||||
import {dismissAllModals, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {permalinkBadTeam} from '@utils/draft';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {displayPermalink} from '@utils/permalink';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
let showingPermalink = false;
|
||||
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, intl: IntlShape, openAsPermalink = true) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
|
|
@ -49,33 +44,10 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId:
|
|||
}
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
if (showingPermalink) {
|
||||
await dismissAllModals();
|
||||
}
|
||||
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
teamName,
|
||||
postId,
|
||||
};
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
},
|
||||
};
|
||||
|
||||
showingPermalink = true;
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
await displayPermalink(team.name, postId, openAsPermalink);
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const closePermalink = () => {
|
||||
showingPermalink = false;
|
||||
};
|
||||
|
|
@ -10,9 +10,11 @@ import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel';
|
|||
import {processPostsFetched, removePost} from '@actions/local/post';
|
||||
import {addRecentReaction} from '@actions/local/reactions';
|
||||
import {ActionType, Events, General, Post, ServerErrors} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {filterPostsInOrderedArray} from '@helpers/api/post';
|
||||
import {getNeededAtMentionedUsernames} from '@helpers/api/user';
|
||||
import {extractRecordsForTable} from '@helpers/database';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMissingChannelsForAllTeams, queryAllMyChannelIds} from '@queries/servers/channel';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
|
|
@ -35,6 +37,13 @@ type PostsRequest = {
|
|||
previousPostId?: string;
|
||||
}
|
||||
|
||||
type PostsObjectsRequest = {
|
||||
error?: unknown;
|
||||
order?: string[];
|
||||
posts?: IDMappedObjects<Post>;
|
||||
previousPostId?: string;
|
||||
}
|
||||
|
||||
type AuthorsRequest = {
|
||||
authors?: UserProfile[];
|
||||
error?: unknown;
|
||||
|
|
@ -430,6 +439,73 @@ export const fetchPostThread = async (serverUrl: string, postId: string, fetchOn
|
|||
}
|
||||
};
|
||||
|
||||
export async function fetchPostsAround(serverUrl: string, channelId: string, postId: string, perPage = General.POST_AROUND_CHUNK_SIZE) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const [after, post, before] = await Promise.all<PostsObjectsRequest>([
|
||||
client.getPostsAfter(channelId, postId, 0, perPage),
|
||||
client.getPostThread(postId),
|
||||
client.getPostsBefore(channelId, postId, 0, perPage),
|
||||
]);
|
||||
|
||||
const preData: PostResponse = {
|
||||
posts: {
|
||||
...filterPostsInOrderedArray(after.posts, after.order),
|
||||
postId: post.posts![postId],
|
||||
...filterPostsInOrderedArray(before.posts, before.order),
|
||||
},
|
||||
order: [],
|
||||
};
|
||||
|
||||
const data = await processPostsFetched(serverUrl, ActionType.POSTS.RECEIVED_AROUND, preData, true);
|
||||
|
||||
let posts: Model[] = [];
|
||||
const models: Model[] = [];
|
||||
if (data.posts?.length) {
|
||||
try {
|
||||
const {authors} = await fetchPostAuthors(serverUrl, data.posts, true);
|
||||
if (authors?.length) {
|
||||
const userModels = await operator.handleUsers({
|
||||
users: authors,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
models.push(...userModels);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('FETCH AUTHORS ERROR', error);
|
||||
}
|
||||
|
||||
posts = await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_AROUND,
|
||||
...data,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
models.push(...posts);
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('FETCH POSTS AROUND ERROR', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export const postActionWithCookie = async (serverUrl: string, postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import {useIntl} from 'react-intl';
|
|||
import {Alert, StyleSheet, Text, View} from 'react-native';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {showPermalink} from '@actions/local/permalink';
|
||||
import {switchToChannelByName} from '@actions/remote/channel';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import DeepLinkTypes from '@constants/deep_linking';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type Props = {
|
|||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
currentTimezone: string | null;
|
||||
currentUsername: string;
|
||||
highlightedId?: PostModel['id'];
|
||||
highlightPinnedOrSaved?: boolean;
|
||||
isTimezoneEnabled: boolean;
|
||||
lastViewedAt: number;
|
||||
|
|
@ -79,6 +80,7 @@ const PostList = ({
|
|||
currentTimezone,
|
||||
currentUsername,
|
||||
footer,
|
||||
highlightedId,
|
||||
highlightPinnedOrSaved = true,
|
||||
isTimezoneEnabled,
|
||||
lastViewedAt,
|
||||
|
|
@ -96,6 +98,7 @@ const PostList = ({
|
|||
const listRef = useRef<FlatList>(null);
|
||||
const onScrollEndIndexListener = useRef<onScrollEndIndexListenerEvent>();
|
||||
const onViewableItemsChangedListener = useRef<ViewableItemsChangedListenerEvent>();
|
||||
const scrolledToHighlighted = useRef(false);
|
||||
const [offsetY, setOffsetY] = useState(0);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
|
@ -152,11 +155,14 @@ const PostList = ({
|
|||
|
||||
const onScrollToIndexFailed = useCallback((info: ScrollIndexFailed) => {
|
||||
const index = Math.min(info.highestMeasuredFrameIndex, info.index);
|
||||
if (onScrollEndIndexListener.current) {
|
||||
onScrollEndIndexListener.current(index);
|
||||
|
||||
if (!highlightedId) {
|
||||
if (onScrollEndIndexListener.current) {
|
||||
onScrollEndIndexListener.current(index);
|
||||
}
|
||||
scrollToIndex(index);
|
||||
}
|
||||
scrollToIndex(index);
|
||||
}, []);
|
||||
}, [highlightedId]);
|
||||
|
||||
const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => {
|
||||
if (!viewableItems.length) {
|
||||
|
|
@ -286,6 +292,7 @@ const PostList = ({
|
|||
);
|
||||
|
||||
const postProps = {
|
||||
highlight: highlightedId === item.id,
|
||||
highlightPinnedOrSaved,
|
||||
location,
|
||||
nextPost,
|
||||
|
|
@ -314,6 +321,21 @@ const PostList = ({
|
|||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
if (highlightedId && orderedPosts && !scrolledToHighlighted.current) {
|
||||
scrolledToHighlighted.current = true;
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const index = orderedPosts.findIndex((p) => typeof p !== 'string' && p.id === highlightedId);
|
||||
if (index >= 0) {
|
||||
scrollToIndex(index, true);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(t);
|
||||
}, [orderedPosts, highlightedId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostListRefreshControl
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import React, {ReactNode, useMemo, useRef} from 'react';
|
|||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, Platform, StyleProp, View, ViewStyle, TouchableHighlight} from 'react-native';
|
||||
|
||||
import {showPermalink} from '@actions/local/permalink';
|
||||
import {removePost} from '@actions/local/post';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import SystemAvatar from '@components/system_avatar';
|
||||
import SystemHeader from '@components/system_header';
|
||||
|
|
@ -129,7 +129,7 @@ const Post = ({
|
|||
if (post) {
|
||||
if (location === Screens.THREAD) {
|
||||
Keyboard.dismiss();
|
||||
} else if (location === Screens.SEARCH) {
|
||||
} else if ([Screens.SAVED_POSTS, Screens.MENTIONS, Screens.SEARCH].includes(location)) {
|
||||
showPermalink(serverUrl, '', post.id, intl);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ export const POSTS = keyMirror({
|
|||
RECEIVED_SINCE: null,
|
||||
RECEIVED_AFTER: null,
|
||||
RECEIVED_BEFORE: null,
|
||||
RECEIVED_AROUND: null,
|
||||
RECEIVED_NEW: null,
|
||||
RECEIVED_POST_THREAD: null,
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
export default {
|
||||
PAGE_SIZE_DEFAULT: 60,
|
||||
POST_CHUNK_SIZE: 60,
|
||||
POST_AROUND_CHUNK_SIZE: 10,
|
||||
CHANNELS_CHUNK_SIZE: 50,
|
||||
STATUS_INTERVAL: 60000,
|
||||
AUTOCOMPLETE_LIMIT_DEFAULT: 25,
|
||||
|
|
|
|||
18
app/helpers/api/post.ts
Normal file
18
app/helpers/api/post.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function filterPostsInOrderedArray(posts?: IDMappedObjects<Post>, order?: string[]) {
|
||||
const result: IDMappedObjects<Post> = {};
|
||||
|
||||
if (!posts || !order) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const id of order) {
|
||||
if (posts[id]) {
|
||||
result[id] = posts[id];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
9
app/helpers/database/index.ts
Normal file
9
app/helpers/database/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
export const extractRecordsForTable = <T>(records: Model[], tableName: string): T[] => {
|
||||
// @ts-expect-error constructor.table not exposed in type definition
|
||||
return records.filter((r) => r.constructor.table === tableName) as T[];
|
||||
};
|
||||
|
|
@ -62,6 +62,11 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
require('@screens/bottom_sheet').default,
|
||||
);
|
||||
break;
|
||||
case Screens.BROWSE_CHANNELS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/browse_channels').default,
|
||||
);
|
||||
break;
|
||||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
|
|
@ -114,10 +119,8 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.MFA:
|
||||
screen = withIntl(require('@screens/mfa').default);
|
||||
break;
|
||||
case Screens.BROWSE_CHANNELS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/browse_channels').default,
|
||||
);
|
||||
case Screens.PERMALINK:
|
||||
screen = withServerDatabase(require('@screens/permalink').default);
|
||||
break;
|
||||
case Screens.POST_OPTIONS:
|
||||
screen = withServerDatabase(
|
||||
|
|
|
|||
|
|
@ -554,13 +554,17 @@ export async function dismissModal(options?: Options & { componentId: string}) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function dismissAllModals(options: Options = {}) {
|
||||
export async function dismissAllModals() {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Navigation.dismissAllModals(options);
|
||||
const modals = EphemeralStore.navigationModalStack;
|
||||
for await (const modal of modals) {
|
||||
await Navigation.dismissModal(modal, {animations: {dismissModal: {enabled: false}}});
|
||||
}
|
||||
|
||||
EphemeralStore.clearNavigationModals();
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there are no modals to
|
||||
|
|
|
|||
36
app/screens/permalink/index.ts
Normal file
36
app/screens/permalink/index.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES} from '@app/constants/database';
|
||||
import {WithDatabaseArgs} from '@typings/database/database';
|
||||
import PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const {POST} = MM_TABLES.SERVER;
|
||||
|
||||
import Permalink from './permalink';
|
||||
|
||||
type OwnProps = {postId: PostModel['id']} & WithDatabaseArgs;
|
||||
|
||||
const enhance = withObservables([], ({database, postId}: OwnProps) => {
|
||||
const post = database.get<PostModel>(POST).query(
|
||||
Q.where('id', postId),
|
||||
).observe().pipe(
|
||||
switchMap((p) => {
|
||||
return p.length ? p[0].observe() : of$(undefined);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
channel: post.pipe(
|
||||
switchMap((p) => (p ? p.channel.observe() : of$(undefined))),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhance(Permalink));
|
||||
231
app/screens/permalink/permalink.tsx
Normal file
231
app/screens/permalink/permalink.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {BackHandler, Text, TouchableOpacity, View} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostsAround} from '@actions/remote/post';
|
||||
import {Screens} from '@app/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import PostList from '@components/post_list';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import ChannelModel from '@typings/database/models/servers/channel';
|
||||
import PostModel from '@typings/database/models/servers/post';
|
||||
import {closePermalink} from '@utils/permalink';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
currentUsername: UserProfile['username'];
|
||||
postId: PostModel['id'];
|
||||
channel?: ChannelModel;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginTop: 20,
|
||||
},
|
||||
wrapper: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 6,
|
||||
flex: 1,
|
||||
margin: 10,
|
||||
opacity: 1,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
paddingRight: 16,
|
||||
width: '100%',
|
||||
},
|
||||
dividerContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
},
|
||||
close: {
|
||||
justifyContent: 'center',
|
||||
height: 44,
|
||||
width: 40,
|
||||
paddingLeft: 7,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
paddingRight: 40,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
},
|
||||
postList: {
|
||||
flex: 1,
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
bottom: {
|
||||
borderBottomLeftRadius: 6,
|
||||
borderBottomRightRadius: 6,
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.buttonBg,
|
||||
flexDirection: 'row',
|
||||
height: 43,
|
||||
paddingRight: 16,
|
||||
width: '100%',
|
||||
},
|
||||
jump: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 15,
|
||||
},
|
||||
errorText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 15,
|
||||
},
|
||||
archiveIcon: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 16,
|
||||
paddingRight: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
function Permalink({channel, postId, currentUsername}: Props) {
|
||||
const [posts, setPosts] = useState<PostModel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const containerStyle = useMemo(() =>
|
||||
[style.container, {marginBottom: insets.bottom}],
|
||||
[style, insets.bottom]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (channel?.id) {
|
||||
const data = await fetchPostsAround(serverUrl, channel.id, postId, 5);
|
||||
if (data?.posts) {
|
||||
setLoading(false);
|
||||
setPosts(data.posts);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [channel?.id]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dismissModal({componentId: Screens.PERMALINK});
|
||||
closePermalink();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
handleClose();
|
||||
return true;
|
||||
});
|
||||
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePress = useCallback(preventDoubleTap(() => {
|
||||
if (channel) {
|
||||
switchToChannelById(serverUrl, channel?.id, channel?.teamId);
|
||||
}
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={containerStyle}>
|
||||
<Animated.View style={style.wrapper}>
|
||||
<View style={style.header}>
|
||||
<TouchableOpacity
|
||||
style={style.close}
|
||||
onPress={handleClose}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
size={20}
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={style.titleContainer}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.title}
|
||||
>
|
||||
{channel?.displayName}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.dividerContainer}>
|
||||
<View style={style.divider}/>
|
||||
</View>
|
||||
{loading ? (
|
||||
<View style={style.loading}>
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={style.postList}>
|
||||
<PostList
|
||||
highlightedId={postId}
|
||||
posts={posts}
|
||||
location={Screens.PERMALINK}
|
||||
lastViewedAt={0}
|
||||
isTimezoneEnabled={false}
|
||||
shouldShowJoinLeaveMessages={false}
|
||||
currentTimezone={null}
|
||||
currentUsername={currentUsername}
|
||||
channelId={channel!.id}
|
||||
testID='permalink.post_list'
|
||||
nativeID={Screens.PERMALINK}
|
||||
highlightPinnedOrSaved={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[style.footer, style.bottom]}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<FormattedText
|
||||
testID='permalink.search.jump'
|
||||
id='mobile.search.jump'
|
||||
defaultMessage='Jump to recent messages'
|
||||
style={style.jump}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default Permalink;
|
||||
36
app/utils/permalink/index.ts
Normal file
36
app/utils/permalink/index.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {dismissAllModals, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
let showingPermalink = false;
|
||||
|
||||
export const displayPermalink = async (teamName: string, postId: string, openAsPermalink = true) => {
|
||||
Keyboard.dismiss();
|
||||
if (showingPermalink) {
|
||||
await dismissAllModals();
|
||||
}
|
||||
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
teamName,
|
||||
postId,
|
||||
};
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
},
|
||||
};
|
||||
|
||||
showingPermalink = true;
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
};
|
||||
|
||||
export const closePermalink = () => {
|
||||
showingPermalink = false;
|
||||
};
|
||||
|
|
@ -337,6 +337,7 @@
|
|||
"mobile.routes.user_profile": "Profile",
|
||||
"mobile.screen.saved_posts": "Saved Messages",
|
||||
"mobile.screen.your_profile": "Your Profile",
|
||||
"mobile.search.jump": "Jump to recent messages",
|
||||
"mobile.server_identifier.exists": "You are already connected to this server.",
|
||||
"mobile.server_link.error.text": "The link could not be found on this server.",
|
||||
"mobile.server_link.error.title": "Link Error",
|
||||
|
|
|
|||
Loading…
Reference in a new issue