forked from Ivasoft/mattermost-mobile
[Gekidou] - Channel Intro (#5846)
* Channel Intro * Move avatar margins to post component per feedback review * Channel intro redesign * Fix preferences unit test * Change group intro sizes * Add Bot tag to DM Intro if they have it * fix channel isTablet layout on split screen * update snapshot
This commit is contained in:
@@ -44,7 +44,7 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team
|
||||
}
|
||||
|
||||
if (system.currentChannelId !== channelId) {
|
||||
const history = await addChannelToTeamHistory(operator, system.currentTeamId, channelId, true);
|
||||
const history = await addChannelToTeamHistory(operator, channel.teamId || system.currentTeamId, channelId, true);
|
||||
models.push(...history);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam, queryMyChannel} from '@queries/servers/channel';
|
||||
import {queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareMyChannelsForTeam, queryChannelById, queryMyChannel} from '@queries/servers/channel';
|
||||
import {queryCommonSystemValues, queryCurrentUserId} from '@queries/servers/system';
|
||||
import {prepareMyTeams, queryMyTeamById, queryTeamById, queryTeamByName} from '@queries/servers/team';
|
||||
import MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
@@ -47,13 +47,23 @@ export const addMembersToChannel = async (serverUrl: string, channelId: string,
|
||||
try {
|
||||
const promises = userIds.map((id) => client.addToChannel(id, channelId, postRootId));
|
||||
const channelMemberships: ChannelMembership[] = await Promise.all(promises);
|
||||
await fetchUsersByIds(serverUrl, userIds, false);
|
||||
const {users} = await fetchUsersByIds(serverUrl, userIds, true);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.handleChannelMembership({
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (users) {
|
||||
modelPromises.push(operator.handleUsers({
|
||||
users,
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
}
|
||||
modelPromises.push(operator.handleChannelMembership({
|
||||
channelMemberships,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
return {channelMemberships};
|
||||
} catch (error) {
|
||||
@@ -87,6 +97,53 @@ export const fetchChannelByName = async (serverUrl: string, teamId: string, chan
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchChannelCreator = async (serverUrl: string, channelId: string, fetchOnly = false) => {
|
||||
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 currentUserId = await queryCurrentUserId(operator.database);
|
||||
const channel = await queryChannelById(operator.database, channelId);
|
||||
if (channel && channel.creatorId) {
|
||||
const user = await client.getUser(channel.creatorId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (user.id !== currentUserId) {
|
||||
modelPromises.push(operator.handleUsers({
|
||||
users: [user],
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
}
|
||||
|
||||
modelPromises.push(operator.handleChannelMembership({
|
||||
channelMemberships: [{channel_id: channelId, user_id: channel.creatorId}],
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
|
||||
return {user};
|
||||
}
|
||||
|
||||
return {user: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string, includeDeleted = true, since = 0, fetchOnly = false, excludeDirect = false): Promise<MyChannelsRequest> => {
|
||||
let client: Client;
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
@@ -25,9 +27,10 @@ export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false):
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
operator.handlePreferences({
|
||||
await operator.handlePreferences({
|
||||
prepareRecordsOnly: false,
|
||||
preferences,
|
||||
sync: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,3 +41,39 @@ export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false):
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const saveFavoriteChannel = async (serverUrl: string, channelId: string, isFavorite: boolean) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
// Todo: @shaz I think you'll need to add the category handler here so that the channel is added/removed from the favorites category
|
||||
const userId = await queryCurrentUserId(operator.database);
|
||||
const favPref: PreferenceType = {
|
||||
category: Preferences.CATEGORY_FAVORITE_CHANNEL,
|
||||
name: channelId,
|
||||
user_id: userId,
|
||||
value: String(isFavorite),
|
||||
};
|
||||
const preferences = [favPref];
|
||||
client.savePreferences(userId, preferences);
|
||||
await operator.handlePreferences({
|
||||
preferences,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
return {preferences};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false
|
||||
}
|
||||
|
||||
if (systems.length) {
|
||||
operator.handleSystem({systems, prepareRecordsOnly: false}).
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false}).
|
||||
catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('An error ocurred while saving config & license', error);
|
||||
|
||||
@@ -48,7 +48,7 @@ export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyU
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,20 +70,32 @@ export const fetchProfilesInChannel = async (serverUrl: string, channelId: strin
|
||||
try {
|
||||
const users = await client.getProfilesInChannel(channelId);
|
||||
const uniqueUsers = Array.from(new Set(users));
|
||||
const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId);
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
const prepare = prepareUsers(operator, uniqueUsers.filter((u) => u.id !== excludeUserId));
|
||||
if (operator && filteredUsers.length) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const membership = filteredUsers.map((u) => ({
|
||||
channel_id: channelId,
|
||||
user_id: u.id,
|
||||
}));
|
||||
modelPromises.push(operator.handleChannelMembership({
|
||||
channelMemberships: membership,
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
const prepare = prepareUsers(operator, filteredUsers);
|
||||
if (prepare) {
|
||||
const models = await prepare;
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
modelPromises.push(prepare);
|
||||
}
|
||||
|
||||
if (modelPromises.length) {
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {channelId, users: uniqueUsers};
|
||||
return {channelId, users: filteredUsers};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {channelId, error};
|
||||
@@ -98,18 +110,29 @@ export const fetchProfilesPerChannels = async (serverUrl: string, channelIds: st
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const users = new Set<UserProfile>();
|
||||
const memberships: Array<{channel_id: string; user_id: string}> = [];
|
||||
for (const item of data) {
|
||||
if (item.users?.length) {
|
||||
item.users.forEach(users.add, users);
|
||||
item.users.forEach((u) => {
|
||||
users.add(u);
|
||||
memberships.push({channel_id: item.channelId, user_id: u.id});
|
||||
});
|
||||
}
|
||||
}
|
||||
modelPromises.push(operator.handleChannelMembership({
|
||||
channelMemberships: memberships,
|
||||
prepareRecordsOnly: true,
|
||||
}));
|
||||
const prepare = prepareUsers(operator, Array.from(users).filter((u) => u.id !== excludeUserId));
|
||||
if (prepare) {
|
||||
const models = await prepare;
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
modelPromises.push(prepare);
|
||||
}
|
||||
|
||||
if (modelPromises.length) {
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ async function doReconnect(serverUrl: string) {
|
||||
const lastDisconnectedAt = await queryWebSocketLastDisconnected(database.database);
|
||||
|
||||
// TODO consider fetch only and batch all the results.
|
||||
fetchMe(serverUrl);
|
||||
fetchMyPreferences(serverUrl);
|
||||
await fetchMe(serverUrl);
|
||||
await fetchMyPreferences(serverUrl);
|
||||
const {config} = await fetchConfigAndLicense(serverUrl);
|
||||
const {memberships: teamMemberships, error: teamMembershipsError} = await fetchMyTeams(serverUrl);
|
||||
const {currentChannelId, currentUserId, currentTeamId, license} = system;
|
||||
@@ -82,7 +82,7 @@ async function doReconnect(serverUrl: string) {
|
||||
|
||||
let channelMemberships: ChannelMembership[] | undefined;
|
||||
if (currentTeamMembership) {
|
||||
const {memberships, channels, error} = await fetchMyChannelsForTeam(serverUrl, currentTeamMembership.team_id, true, lastDisconnectedAt, true);
|
||||
const {memberships, channels, error} = await fetchMyChannelsForTeam(serverUrl, currentTeamMembership.team_id, true, lastDisconnectedAt);
|
||||
if (error) {
|
||||
DeviceEventEmitter.emit(Events.TEAM_LOAD_ERROR, serverUrl, error);
|
||||
return;
|
||||
@@ -92,7 +92,7 @@ async function doReconnect(serverUrl: string) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, license);
|
||||
const directChannels = channels?.filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
|
||||
if (directChannels?.length) {
|
||||
await fetchMissingSidebarInfo(serverUrl, directChannels, currentUser?.locale, teammateDisplayNameSetting, currentUserId, true);
|
||||
await fetchMissingSidebarInfo(serverUrl, directChannels, currentUser?.locale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`components/channel_list should match snapshot 1`] = `
|
||||
animatedStyle={
|
||||
Object {
|
||||
"value": Object {
|
||||
"maxWidth": undefined,
|
||||
"maxWidth": "100%",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ exports[`components/channel_list should match snapshot 1`] = `
|
||||
Object {
|
||||
"backgroundColor": "#1e325c",
|
||||
"flex": 1,
|
||||
"maxWidth": undefined,
|
||||
"maxWidth": "100%",
|
||||
"paddingHorizontal": 20,
|
||||
"paddingVertical": 10,
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ const ChannelList = ({iconPad, isTablet, teamsCount}: ChannelListProps) => {
|
||||
const tabletStyle = useAnimatedStyle(() => {
|
||||
if (!isTablet) {
|
||||
return {
|
||||
maxWidth: undefined,
|
||||
maxWidth: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
return {maxWidth: withTiming(tabletWidth.value, {duration: 350})};
|
||||
}, []);
|
||||
}, [isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTablet) {
|
||||
@@ -64,7 +64,6 @@ const ChannelList = ({iconPad, isTablet, teamsCount}: ChannelListProps) => {
|
||||
}, [isTablet, teamsCount]);
|
||||
|
||||
const [showCats, setShowCats] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, tabletStyle]}>
|
||||
<TouchableOpacity onPress={() => setShowCats(!showCats)}>
|
||||
|
||||
@@ -4,23 +4,17 @@
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {ReactElement, useCallback} from 'react';
|
||||
import {AppStateStatus, DeviceEventEmitter, FlatList, Platform, RefreshControl, StyleSheet, ViewToken} from 'react-native';
|
||||
import {AppStateStatus} from 'react-native';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import CombinedUserActivity from '@components/post_list/combined_user_activity';
|
||||
import DateSeparator from '@components/post_list/date_separator';
|
||||
import NewMessagesLine from '@components/post_list/new_message_line';
|
||||
import Post from '@components/post_list/post';
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {getDateForDateLine, isCombinedUserActivityPost, isDateLine, isStartOfNewMessages, preparePostList} from '@utils/post_list';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import PostList from './post_list';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
@@ -29,42 +23,6 @@ import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type RefreshProps = {
|
||||
children: ReactElement;
|
||||
enabled: boolean;
|
||||
onRefresh: () => void;
|
||||
refreshing: boolean;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
currentTimezone: string | null;
|
||||
currentUsername: string;
|
||||
isTimezoneEnabled: boolean;
|
||||
lastViewedAt: number;
|
||||
posts: PostModel[];
|
||||
shouldShowJoinLeaveMessages: boolean;
|
||||
testID: string;
|
||||
}
|
||||
|
||||
type ViewableItemsChanged = {
|
||||
viewableItems: ViewToken[];
|
||||
changed: ViewToken[];
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
scaleY: -1,
|
||||
},
|
||||
scale: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
scaleY: -1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const {SERVER: {MY_CHANNEL, POST, POSTS_IN_CHANNEL, PREFERENCE, SYSTEM, USER}} = MM_TABLES;
|
||||
|
||||
export const VIEWABILITY_CONFIG = {
|
||||
@@ -72,161 +30,7 @@ export const VIEWABILITY_CONFIG = {
|
||||
minimumViewTime: 100,
|
||||
};
|
||||
|
||||
const PostListRefreshControl = ({children, enabled, onRefresh, refreshing}: RefreshProps) => {
|
||||
const props = {
|
||||
onRefresh,
|
||||
refreshing,
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<RefreshControl
|
||||
{...props}
|
||||
enabled={enabled}
|
||||
style={style.container}
|
||||
>
|
||||
{children}
|
||||
</RefreshControl>
|
||||
);
|
||||
}
|
||||
|
||||
const refreshControl = <RefreshControl {...props}/>;
|
||||
|
||||
return React.cloneElement(
|
||||
children,
|
||||
{refreshControl, inverted: true},
|
||||
);
|
||||
};
|
||||
|
||||
const PostList = ({currentTimezone, currentUsername, isTimezoneEnabled, lastViewedAt, posts, shouldShowJoinLeaveMessages, testID}: Props) => {
|
||||
const theme = useTheme();
|
||||
const orderedPosts = preparePostList(posts, lastViewedAt, true, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, currentTimezone, false);
|
||||
|
||||
const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => {
|
||||
if (!viewableItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
|
||||
if (isViewable) {
|
||||
acc[item.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
DeviceEventEmitter.emit('scrolled', viewableItemsMap);
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(({item, index}) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isStartOfNewMessages(item)) {
|
||||
// postIds includes a date item after the new message indicator so 2
|
||||
// needs to be added to the index for the length check to be correct.
|
||||
const moreNewMessages = orderedPosts.length === index + 2;
|
||||
|
||||
// The date line and new message line each count for a line. So the
|
||||
// goal of this is to check for the 3rd previous, which for the start
|
||||
// of a thread would be null as it doesn't exist.
|
||||
const checkForPostId = index < orderedPosts.length - 3;
|
||||
|
||||
return (
|
||||
<NewMessagesLine
|
||||
theme={theme}
|
||||
moreMessages={moreNewMessages && checkForPostId}
|
||||
testID={`${testID}.new_messages_line`}
|
||||
style={style.scale}
|
||||
/>
|
||||
);
|
||||
} else if (isDateLine(item)) {
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
theme={theme}
|
||||
style={style.scale}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCombinedUserActivityPost(item)) {
|
||||
const postProps = {
|
||||
currentUsername,
|
||||
postId: item,
|
||||
style: Platform.OS === 'ios' ? style.scale : style.container,
|
||||
testID: `${testID}.combined_user_activity`,
|
||||
showJoinLeave: shouldShowJoinLeaveMessages,
|
||||
theme,
|
||||
};
|
||||
|
||||
return (<CombinedUserActivity {...postProps}/>);
|
||||
}
|
||||
}
|
||||
|
||||
let previousPost: PostModel|undefined;
|
||||
let nextPost: PostModel|undefined;
|
||||
if (index < posts.length - 1) {
|
||||
const prev = orderedPosts.slice(index + 1).find((v) => typeof v !== 'string');
|
||||
if (prev) {
|
||||
previousPost = prev as PostModel;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
const next = orderedPosts.slice(0, index);
|
||||
for (let i = next.length - 1; i >= 0; i--) {
|
||||
const v = next[i];
|
||||
if (typeof v !== 'string') {
|
||||
nextPost = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const postProps = {
|
||||
highlightPinnedOrFlagged: true,
|
||||
location: 'Channel',
|
||||
nextPost,
|
||||
previousPost,
|
||||
shouldRenderReplyButton: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Post
|
||||
key={item.id}
|
||||
post={item}
|
||||
style={style.scale}
|
||||
testID={`${testID}.post`}
|
||||
{...postProps}
|
||||
/>
|
||||
);
|
||||
}, [orderedPosts, theme]);
|
||||
|
||||
return (
|
||||
<PostListRefreshControl
|
||||
enabled={false}
|
||||
refreshing={false}
|
||||
onRefresh={emptyFunction}
|
||||
>
|
||||
<FlatList
|
||||
data={orderedPosts}
|
||||
renderItem={renderItem}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
keyExtractor={(item) => (typeof item === 'string' ? item : item.id)}
|
||||
style={{flex: 1}}
|
||||
contentContainerStyle={{paddingTop: 5}}
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
removeClippedSubviews={true}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={VIEWABILITY_CONFIG}
|
||||
scrollEventThrottle={60}
|
||||
/>
|
||||
</PostListRefreshControl>
|
||||
);
|
||||
};
|
||||
|
||||
const withPosts = withObservables(['channelId', 'forceQueryAfterAppState'], ({database, channelId}: {channelId: string; forceQueryAfterAppState: AppStateStatus} & WithDatabaseArgs) => {
|
||||
const enhanced = withObservables(['channelId', 'forceQueryAfterAppState'], ({database, channelId}: {channelId: string; forceQueryAfterAppState: AppStateStatus} & WithDatabaseArgs) => {
|
||||
const currentUser = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap((currentUserId) => database.get<UserModel>(USER).findAndObserve(currentUserId.value)),
|
||||
);
|
||||
@@ -269,4 +73,4 @@ const withPosts = withObservables(['channelId', 'forceQueryAfterAppState'], ({da
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withPosts(PostList));
|
||||
export default withDatabase(enhanced(PostList));
|
||||
|
||||
@@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
consecutivePostContainer: {
|
||||
marginBottom: 10,
|
||||
marginRight: 10,
|
||||
marginLeft: 47,
|
||||
marginLeft: 27,
|
||||
marginTop: 10,
|
||||
},
|
||||
container: {flexDirection: 'row'},
|
||||
@@ -76,6 +76,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
},
|
||||
profilePictureContainer: {
|
||||
marginBottom: 5,
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
},
|
||||
replyBar: {
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
opacity: 0.1,
|
||||
@@ -89,7 +94,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
rightColumn: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
marginRight: 12,
|
||||
},
|
||||
rightColumnPadding: {paddingBottom: 3},
|
||||
};
|
||||
@@ -186,15 +190,18 @@ const Post = ({
|
||||
consecutiveStyle = styles.consective;
|
||||
postAvatar = <View style={styles.consecutivePostContainer}/>;
|
||||
} else {
|
||||
postAvatar = isAutoResponder ? (
|
||||
<SystemAvatar theme={theme}/>
|
||||
) : (
|
||||
<Avatar
|
||||
isAutoReponse={isAutoResponder}
|
||||
isSystemPost={isSystemPost}
|
||||
pendingPostStyle={pendingPostStyle}
|
||||
post={post}
|
||||
/>
|
||||
postAvatar = (
|
||||
<View style={[styles.profilePictureContainer, pendingPostStyle]}>
|
||||
{isAutoResponder ? (
|
||||
<SystemAvatar theme={theme}/>
|
||||
) : (
|
||||
<Avatar
|
||||
isAutoReponse={isAutoResponder}
|
||||
isSystemPost={isSystemPost}
|
||||
post={post}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (isSystemPost && !isAutoResponder) {
|
||||
|
||||
@@ -18,8 +18,10 @@ exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] =
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -37,8 +39,10 @@ exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] =
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
@@ -67,8 +71,10 @@ exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -86,8 +92,10 @@ exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
@@ -103,8 +111,10 @@ exports[`renderSystemMessage uses renderer for Channel Purpose update 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -130,8 +140,10 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 1
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -149,8 +161,10 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 1
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
@@ -179,8 +193,10 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -198,8 +214,10 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
@@ -211,8 +229,10 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -247,8 +267,10 @@ exports[`renderSystemMessage uses renderer for OLD archived channel without a us
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
@@ -277,8 +299,10 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -296,8 +320,10 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
@@ -326,8 +352,10 @@ exports[`renderSystemMessage uses renderer for unarchived channel 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
},
|
||||
Object {
|
||||
"opacity": 1,
|
||||
@@ -345,8 +373,10 @@ exports[`renderSystemMessage uses renderer for unarchived channel 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 20,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
|
||||
@@ -11,6 +11,7 @@ import {useTheme} from '@context/theme';
|
||||
import {t} from '@i18n';
|
||||
import {getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
@@ -46,8 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
systemMessage: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 16,
|
||||
lineHeight: 20,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
233
app/components/post_list/post_list.tsx
Normal file
233
app/components/post_list/post_list.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactElement, useCallback, useEffect, useRef} from 'react';
|
||||
import {DeviceEventEmitter, FlatList, Platform, RefreshControl, StyleProp, StyleSheet, ViewStyle, ViewToken} from 'react-native';
|
||||
|
||||
import CombinedUserActivity from '@components/post_list/combined_user_activity';
|
||||
import DateSeparator from '@components/post_list/date_separator';
|
||||
import NewMessagesLine from '@components/post_list/new_message_line';
|
||||
import Post from '@components/post_list/post';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {getDateForDateLine, isCombinedUserActivityPost, isDateLine, isStartOfNewMessages, preparePostList} from '@utils/post_list';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type RefreshProps = {
|
||||
children: ReactElement;
|
||||
enabled: boolean;
|
||||
onRefresh: () => void;
|
||||
refreshing: boolean;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
currentTimezone: string | null;
|
||||
currentUsername: string;
|
||||
isTimezoneEnabled: boolean;
|
||||
lastViewedAt: number;
|
||||
posts: PostModel[];
|
||||
shouldShowJoinLeaveMessages: boolean;
|
||||
footer?: ReactElement;
|
||||
testID: string;
|
||||
}
|
||||
|
||||
type ViewableItemsChanged = {
|
||||
viewableItems: ViewToken[];
|
||||
changed: ViewToken[];
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
scaleY: -1,
|
||||
},
|
||||
scale: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
scaleY: -1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const VIEWABILITY_CONFIG = {
|
||||
itemVisiblePercentThreshold: 1,
|
||||
minimumViewTime: 100,
|
||||
};
|
||||
|
||||
const keyExtractor = (item: string | PostModel) => (typeof item === 'string' ? item : item.id);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
});
|
||||
|
||||
const PostListRefreshControl = ({children, enabled, onRefresh, refreshing}: RefreshProps) => {
|
||||
const props = {
|
||||
onRefresh,
|
||||
refreshing,
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<RefreshControl
|
||||
{...props}
|
||||
enabled={enabled}
|
||||
style={style.container}
|
||||
>
|
||||
{children}
|
||||
</RefreshControl>
|
||||
);
|
||||
}
|
||||
|
||||
const refreshControl = <RefreshControl {...props}/>;
|
||||
|
||||
return React.cloneElement(
|
||||
children,
|
||||
{refreshControl, inverted: true},
|
||||
);
|
||||
};
|
||||
|
||||
const PostList = ({channelId, contentContainerStyle, currentTimezone, currentUsername, footer, isTimezoneEnabled, lastViewedAt, posts, shouldShowJoinLeaveMessages, testID}: Props) => {
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const theme = useTheme();
|
||||
const orderedPosts = preparePostList(posts, lastViewedAt, true, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, currentTimezone, false);
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false});
|
||||
}, [channelId, listRef.current]);
|
||||
|
||||
const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => {
|
||||
if (!viewableItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
|
||||
if (isViewable) {
|
||||
acc[item.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
DeviceEventEmitter.emit('scrolled', viewableItemsMap);
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(({item, index}) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isStartOfNewMessages(item)) {
|
||||
// postIds includes a date item after the new message indicator so 2
|
||||
// needs to be added to the index for the length check to be correct.
|
||||
const moreNewMessages = orderedPosts.length === index + 2;
|
||||
|
||||
// The date line and new message line each count for a line. So the
|
||||
// goal of this is to check for the 3rd previous, which for the start
|
||||
// of a thread would be null as it doesn't exist.
|
||||
const checkForPostId = index < orderedPosts.length - 3;
|
||||
|
||||
return (
|
||||
<NewMessagesLine
|
||||
theme={theme}
|
||||
moreMessages={moreNewMessages && checkForPostId}
|
||||
testID={`${testID}.new_messages_line`}
|
||||
style={style.scale}
|
||||
/>
|
||||
);
|
||||
} else if (isDateLine(item)) {
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
theme={theme}
|
||||
style={style.scale}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCombinedUserActivityPost(item)) {
|
||||
const postProps = {
|
||||
currentUsername,
|
||||
postId: item,
|
||||
style: Platform.OS === 'ios' ? style.scale : style.container,
|
||||
testID: `${testID}.combined_user_activity`,
|
||||
showJoinLeave: shouldShowJoinLeaveMessages,
|
||||
theme,
|
||||
};
|
||||
|
||||
return (<CombinedUserActivity {...postProps}/>);
|
||||
}
|
||||
}
|
||||
|
||||
let previousPost: PostModel|undefined;
|
||||
let nextPost: PostModel|undefined;
|
||||
if (index < posts.length - 1) {
|
||||
const prev = orderedPosts.slice(index + 1).find((v) => typeof v !== 'string');
|
||||
if (prev) {
|
||||
previousPost = prev as PostModel;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
const next = orderedPosts.slice(0, index);
|
||||
for (let i = next.length - 1; i >= 0; i--) {
|
||||
const v = next[i];
|
||||
if (typeof v !== 'string') {
|
||||
nextPost = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const postProps = {
|
||||
highlightPinnedOrFlagged: true,
|
||||
location: 'Channel',
|
||||
nextPost,
|
||||
previousPost,
|
||||
shouldRenderReplyButton: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Post
|
||||
key={item.id}
|
||||
post={item}
|
||||
style={style.scale}
|
||||
testID={`${testID}.post`}
|
||||
{...postProps}
|
||||
/>
|
||||
);
|
||||
}, [orderedPosts, theme]);
|
||||
|
||||
return (
|
||||
<PostListRefreshControl
|
||||
enabled={false}
|
||||
refreshing={false}
|
||||
onRefresh={emptyFunction}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={[styles.content, contentContainerStyle]}
|
||||
data={orderedPosts}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
keyExtractor={keyExtractor}
|
||||
initialNumToRender={10}
|
||||
ListFooterComponent={footer}
|
||||
maxToRenderPerBatch={10}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
removeClippedSubviews={true}
|
||||
scrollEventThrottle={60}
|
||||
style={styles.flex}
|
||||
viewabilityConfig={VIEWABILITY_CONFIG}
|
||||
/>
|
||||
</PostListRefreshControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostList;
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, View, ViewStyle} from 'react-native';
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
@@ -16,14 +16,15 @@ type TagProps = {
|
||||
show?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 2,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
borderRadius: 4,
|
||||
marginRight: 2,
|
||||
marginBottom: 1,
|
||||
marginLeft: 2,
|
||||
@@ -68,7 +69,7 @@ export function GuestTag(props: Omit<TagProps, 'id' | 'defaultMessage'>) {
|
||||
);
|
||||
}
|
||||
|
||||
const Tag = ({id, defaultMessage, inTitle, show = true, style, testID}: TagProps) => {
|
||||
const Tag = ({id, defaultMessage, inTitle, show = true, style, testID, textStyle}: TagProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!show) {
|
||||
@@ -82,7 +83,7 @@ const Tag = ({id, defaultMessage, inTitle, show = true, style, testID}: TagProps
|
||||
<FormattedText
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
style={[styles.text, inTitle ? styles.title : null]}
|
||||
style={[styles.text, inTitle ? styles.title : null, textStyle]}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -6,6 +6,9 @@ export const EMOJI_PICKER = 'AddReaction';
|
||||
export const APP_FORM = 'AppForm';
|
||||
export const BOTTOM_SHEET = 'BottomSheet';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_DETAILS = 'ChannelDetails';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const FORGOT_PASSWORD = 'ForgotPassword';
|
||||
@@ -21,6 +24,7 @@ export const SERVER = 'Server';
|
||||
export const SETTINGS_SIDEBAR = 'SettingsSidebar';
|
||||
export const SSO = 'SSO';
|
||||
export const THREAD = 'Thread';
|
||||
export const USER_PROFILE = 'UserProfile';
|
||||
export const MENTIONS = 'Mentions';
|
||||
|
||||
export default {
|
||||
@@ -29,6 +33,9 @@ export default {
|
||||
APP_FORM,
|
||||
BOTTOM_SHEET,
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_EDIT,
|
||||
CHANNEL_DETAILS,
|
||||
CUSTOM_STATUS_CLEAR_AFTER,
|
||||
CUSTOM_STATUS,
|
||||
FORGOT_PASSWORD,
|
||||
@@ -44,5 +51,6 @@ export default {
|
||||
SETTINGS_SIDEBAR,
|
||||
SSO,
|
||||
THREAD,
|
||||
USER_PROFILE,
|
||||
MENTIONS,
|
||||
};
|
||||
|
||||
@@ -151,9 +151,8 @@ describe('*** Operator: User Handlers tests ***', () => {
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'user_id',
|
||||
createOrUpdateRawValues: preferences,
|
||||
deleteRawValues: [],
|
||||
tableName: 'Preference',
|
||||
prepareRecordsOnly: false,
|
||||
prepareRecordsOnly: true,
|
||||
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
|
||||
transformer: transformPreferenceRecord,
|
||||
});
|
||||
@@ -163,6 +162,7 @@ describe('*** Operator: User Handlers tests ***', () => {
|
||||
expect.assertions(2);
|
||||
const channelMemberships: ChannelMembership[] = [
|
||||
{
|
||||
id: '17bfnb1uwb8epewp4q3x3rx9go-9ciscaqbrpd6d8s68k76xb9bte',
|
||||
channel_id: '17bfnb1uwb8epewp4q3x3rx9go',
|
||||
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
|
||||
roles: 'wqyby5r5pinxxdqhoaomtacdhc',
|
||||
@@ -181,6 +181,7 @@ describe('*** Operator: User Handlers tests ***', () => {
|
||||
scheme_admin: false,
|
||||
},
|
||||
{
|
||||
id: '1yw6gxfr4bn1jbyp9nr7d53yew-9ciscaqbrpd6d8s68k76xb9bte',
|
||||
channel_id: '1yw6gxfr4bn1jbyp9nr7d53yew',
|
||||
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
|
||||
roles: 'channel_user',
|
||||
|
||||
@@ -59,7 +59,12 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||
);
|
||||
}
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: channelMemberships, key: 'channel_id'});
|
||||
const memberships: ChannelMember[] = channelMemberships.map((m) => ({
|
||||
id: `${m.channel_id}-${m.user_id}`,
|
||||
...m,
|
||||
}));
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: memberships, key: 'id'});
|
||||
|
||||
return this.handleRecords({
|
||||
fieldName: 'user_id',
|
||||
@@ -87,33 +92,36 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||
}
|
||||
|
||||
// WE NEED TO SYNC THE PREFS FROM WHAT WE GOT AND WHAT WE HAVE
|
||||
const deleteRawValues: PreferenceType[] = [];
|
||||
const deleteValues: PreferenceModel[] = [];
|
||||
if (sync) {
|
||||
const stored = await this.database.get(PREFERENCE).fetch() as PreferenceModel[];
|
||||
const stored = await this.database.get(PREFERENCE).query().fetch() as PreferenceModel[];
|
||||
for (const pref of stored) {
|
||||
const exists = preferences.findIndex((p) => p.category === pref.category && p.name === pref.name) > -1;
|
||||
if (!exists) {
|
||||
deleteRawValues.push({
|
||||
category: pref.category,
|
||||
name: pref.name,
|
||||
user_id: pref.userId,
|
||||
value: pref.value,
|
||||
});
|
||||
pref.prepareDestroyPermanently();
|
||||
deleteValues.push(pref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: preferences, key: 'name'});
|
||||
|
||||
return this.handleRecords({
|
||||
const records: PreferenceModel[] = await this.handleRecords({
|
||||
fieldName: 'user_id',
|
||||
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
|
||||
transformer: transformPreferenceRecord,
|
||||
prepareRecordsOnly,
|
||||
createOrUpdateRawValues,
|
||||
deleteRawValues,
|
||||
prepareRecordsOnly: true,
|
||||
createOrUpdateRawValues: preferences,
|
||||
tableName: PREFERENCE,
|
||||
});
|
||||
|
||||
if (deleteValues.length) {
|
||||
records.push(...deleteValues);
|
||||
}
|
||||
|
||||
if (records.length && !prepareRecordsOnly) {
|
||||
await this.batchRecords(records);
|
||||
}
|
||||
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ export const DEFAULT_LOCALE = deviceLocale;
|
||||
|
||||
function loadTranslation(locale?: string) {
|
||||
try {
|
||||
let translations;
|
||||
let translations: Record<string, string>;
|
||||
let momentData;
|
||||
|
||||
switch (locale) {
|
||||
@@ -184,7 +184,7 @@ export function getLocalizedMessage(lang: string, id: string, defaultMessage?: s
|
||||
const locale = getLocaleFromLanguage(lang);
|
||||
const translations = getTranslations(locale);
|
||||
|
||||
return translations[id] || defaultMessage;
|
||||
return translations[id] || defaultMessage || '';
|
||||
}
|
||||
|
||||
export function t(v: string): string {
|
||||
|
||||
@@ -140,6 +140,15 @@ export const queryMyChannel = async (database: Database, channelId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const queryChannelById = async (database: Database, channelId: string) => {
|
||||
try {
|
||||
const channel = await database.get<ChannelModel>(CHANNEL).find(channelId);
|
||||
return channel;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryChannelByName = async (database: Database, channelName: string) => {
|
||||
try {
|
||||
const channels = await database.get(CHANNEL).query(Q.where('name', channelName)).fetch() as ChannelModel[];
|
||||
|
||||
@@ -57,7 +57,7 @@ export const prepareModels = async ({operator, initialTeamId, removeTeams, remov
|
||||
}
|
||||
|
||||
if (prefData?.preferences?.length) {
|
||||
const prefModel = prepareMyPreferences(operator, prefData.preferences);
|
||||
const prefModel = prepareMyPreferences(operator, prefData.preferences, true);
|
||||
if (prefModel) {
|
||||
modelPromises.push(prefModel);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import {queryCurrentTeamId} from './system';
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
||||
export const prepareMyPreferences = (operator: ServerDataOperator, preferences: PreferenceType[]) => {
|
||||
export const prepareMyPreferences = (operator: ServerDataOperator, preferences: PreferenceType[], sync = false) => {
|
||||
try {
|
||||
return operator.handlePreferences({
|
||||
prepareRecordsOnly: true,
|
||||
preferences,
|
||||
sync,
|
||||
});
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
@@ -3,25 +3,22 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import {logout} from '@actions/remote/session';
|
||||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import PostList from '@components/post_list';
|
||||
import ServerVersion from '@components/server_version';
|
||||
import {Screens, Database} from '@constants';
|
||||
import {Database} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import {goToScreen} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ChannelNavBar from './channel_nav_bar';
|
||||
import FailedChannels from './failed_channels';
|
||||
import FailedTeams from './failed_teams';
|
||||
import Intro from './intro';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
@@ -30,7 +27,6 @@ import type {LaunchProps} from '@typings/launch';
|
||||
type ChannelProps = LaunchProps & {
|
||||
currentChannelId: string;
|
||||
currentTeamId: string;
|
||||
time?: number;
|
||||
};
|
||||
|
||||
const {MM_TABLES, SYSTEM_IDENTIFIERS} = Database;
|
||||
@@ -51,85 +47,49 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const Channel = ({currentChannelId, currentTeamId, time}: ChannelProps) => {
|
||||
// TODO: If we have LaunchProps, ensure we load the correct channel/post/modal.
|
||||
// TODO: If LaunchProps.error is true, use the LaunchProps.launchType to determine which
|
||||
// error message to display. For example:
|
||||
// if (props.launchError) {
|
||||
// let erroMessage;
|
||||
// if (props.launchType === LaunchType.DeepLink) {
|
||||
// errorMessage = intl.formatMessage({id: 'mobile.launchError.deepLink', defaultMessage: 'Did not find a server for this deep link'});
|
||||
// } else if (props.launchType === LaunchType.Notification) {
|
||||
// errorMessage = intl.formatMessage({id: 'mobile.launchError.notification', defaultMessage: 'Did not find a server for this notification'});
|
||||
// }
|
||||
// }
|
||||
|
||||
//todo: https://mattermost.atlassian.net/browse/MM-37266
|
||||
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const Channel = ({currentChannelId, currentTeamId}: ChannelProps) => {
|
||||
const appState = useAppState();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const doLogout = () => {
|
||||
logout(serverUrl!);
|
||||
};
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchPostsForChannel(serverUrl, currentChannelId).then(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [currentChannelId]);
|
||||
|
||||
const goToAbout = () => {
|
||||
const title = intl.formatMessage({id: 'about.title', defaultMessage: 'About {appTitle}'}, {appTitle: 'Mattermost'});
|
||||
goToScreen(Screens.ABOUT, title);
|
||||
};
|
||||
if (!currentTeamId) {
|
||||
return <FailedTeams/>;
|
||||
}
|
||||
|
||||
const renderComponent = useMemo(() => {
|
||||
if (!currentTeamId) {
|
||||
return <FailedTeams/>;
|
||||
}
|
||||
|
||||
if (!currentChannelId) {
|
||||
return <FailedChannels teamId={currentTeamId}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChannelNavBar
|
||||
channelId={currentChannelId}
|
||||
onPress={() => null}
|
||||
/>
|
||||
<PostList
|
||||
channelId={currentChannelId}
|
||||
testID='channel.post_list'
|
||||
forceQueryAfterAppState={appState}
|
||||
/>
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text
|
||||
onPress={doLogout}
|
||||
style={styles.sectionTitle}
|
||||
>
|
||||
{`Loaded in: ${time || 0}ms. Logout from ${serverUrl}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text
|
||||
onPress={goToAbout}
|
||||
style={styles.sectionTitle}
|
||||
>
|
||||
{'Go to About Screen'}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}, [currentTeamId, currentChannelId, theme, appState]);
|
||||
if (!currentChannelId) {
|
||||
return <FailedChannels teamId={currentTeamId}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
mode='margin'
|
||||
edges={['left', 'right', 'bottom']}
|
||||
edges={['left', 'right']}
|
||||
>
|
||||
<ServerVersion/>
|
||||
{renderComponent}
|
||||
<ChannelNavBar
|
||||
channelId={currentChannelId}
|
||||
onPress={() => null}
|
||||
/>
|
||||
<PostList
|
||||
channelId={currentChannelId}
|
||||
footer={(
|
||||
<Intro
|
||||
channelId={currentChannelId}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
forceQueryAfterAppState={appState}
|
||||
testID='channel.post_list'
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
152
app/screens/channel/intro/direct_channel/direct_channel.tsx
Normal file
152
app/screens/channel/intro/direct_channel/direct_channel.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {fetchProfilesInChannel} from '@actions/remote/user';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {BotTag} from '@components/tag';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import IntroOptions from '../options';
|
||||
|
||||
import Group from './group';
|
||||
import Member from './member';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
currentUserId: string;
|
||||
isBot: boolean;
|
||||
members?: ChannelMembershipModel[];
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
botContainer: {
|
||||
alignSelf: 'flex-end',
|
||||
bottom: 7.5,
|
||||
height: 20,
|
||||
marginBottom: 0,
|
||||
marginLeft: 4,
|
||||
paddingVertical: 0,
|
||||
},
|
||||
botText: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
textAlign: 'center',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
profilesContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
textAlign: 'center',
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
},
|
||||
titleGroup: {
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const DirectChannel = ({channel, currentUserId, isBot, members, theme}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
|
||||
if (!channelMembers?.length) {
|
||||
fetchProfilesInChannel(serverUrl, channel.id, currentUserId, false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
return (
|
||||
<FormattedText
|
||||
defaultMessage={'This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.'}
|
||||
id='intro.direct_message'
|
||||
style={styles.message}
|
||||
values={{teammate: channel.displayName}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormattedText
|
||||
defaultMessage={'This is the start of your conversation with this group. Messages and files shared here are not shown to anyone else outside of the group.'}
|
||||
id='intro.group_message'
|
||||
style={styles.message}
|
||||
/>
|
||||
);
|
||||
}, [channel.displayName, theme]);
|
||||
|
||||
const profiles = useMemo(() => {
|
||||
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
|
||||
if (!channelMembers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
return (
|
||||
<Member
|
||||
containerStyle={{height: 96}}
|
||||
member={channelMembers[0]}
|
||||
size={96}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group
|
||||
theme={theme}
|
||||
userIds={channelMembers.map((cm) => cm.userId)}
|
||||
/>
|
||||
);
|
||||
}, [members, theme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.profilesContainer}>
|
||||
{profiles}
|
||||
</View>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Text style={[styles.title, channel.type === General.GM_CHANNEL ? styles.titleGroup : undefined]}>
|
||||
{channel.displayName}
|
||||
</Text>
|
||||
{isBot &&
|
||||
<BotTag
|
||||
style={styles.botContainer}
|
||||
textStyle={styles.botText}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
{message}
|
||||
<IntroOptions
|
||||
channelId={channel.id}
|
||||
header={true}
|
||||
favorite={true}
|
||||
people={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectChannel;
|
||||
78
app/screens/channel/intro/direct_channel/group/group.tsx
Normal file
78
app/screens/channel/intro/direct_channel/group/group.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {chunk} from 'lodash';
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {useServerUrl} from '@context/server';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
users: UserModel[];
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 12,
|
||||
},
|
||||
profile: {
|
||||
borderColor: theme.centerChannelBg,
|
||||
borderRadius: 36,
|
||||
borderWidth: 2,
|
||||
height: 72,
|
||||
width: 72,
|
||||
},
|
||||
}));
|
||||
|
||||
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);
|
||||
return (
|
||||
<FastImage
|
||||
key={pictureUrl + i.toString()}
|
||||
style={[styles.profile, {transform: [{translateX: -(i * 24)}]}]}
|
||||
source={{uri: `${serverUrl}${pictureUrl}`}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
key={'group_avatar' + k.toString()}
|
||||
style={[styles.container, {left: (c.length - 1) * 12}]}
|
||||
>
|
||||
{group}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{groups}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Group;
|
||||
21
app/screens/channel/intro/direct_channel/group/index.ts
Normal file
21
app/screens/channel/intro/direct_channel/group/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 {MM_TABLES} from '@constants/database';
|
||||
|
||||
import Group from './group';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {USER}} = MM_TABLES;
|
||||
|
||||
const enhanced = withObservables([], ({userIds, database}: {userIds: string[]} & WithDatabaseArgs) => ({
|
||||
users: database.get<UserModel>(USER).query(Q.where('id', Q.oneOf(userIds))).observeWithColumns(['last_picture_update']),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(Group));
|
||||
45
app/screens/channel/intro/direct_channel/index.ts
Normal file
45
app/screens/channel/intro/direct_channel/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {catchError, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@app/constants/database';
|
||||
import {General} from '@constants';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import DirectChannel from './direct_channel';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const enhanced = withObservables([], ({channel, database}: {channel: ChannelModel} & WithDatabaseArgs) => {
|
||||
const currentUserId = database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(switchMap(({value}) => of$(value)));
|
||||
const members = channel.members.observe();
|
||||
let isBot = of$(false);
|
||||
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
isBot = currentUserId.pipe(
|
||||
switchMap((userId: string) => {
|
||||
const otherUserId = getUserIdFromChannelName(userId, channel.name);
|
||||
return database.get<UserModel>(MM_TABLES.SERVER.USER).findAndObserve(otherUserId).pipe(
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
switchMap((user) => of$(user.isBot)), // eslint-disable-next-line max-nested-callbacks
|
||||
catchError(() => of$(false)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
isBot,
|
||||
members,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(DirectChannel));
|
||||
15
app/screens/channel/intro/direct_channel/member/index.ts
Normal file
15
app/screens/channel/intro/direct_channel/member/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import Member from './member';
|
||||
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
||||
const enhanced = withObservables([], ({member}: {member: ChannelMembershipModel}) => ({
|
||||
user: member.memberUser.observe(),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(Member));
|
||||
74
app/screens/channel/intro/direct_channel/member/member.tsx
Normal file
74
app/screens/channel/intro/direct_channel/member/member.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, StyleSheet, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Screens} from '@constants';
|
||||
import {showModal} from '@screens/navigation';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
size?: number;
|
||||
showStatus?: boolean;
|
||||
theme: Theme;
|
||||
user: UserModel;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
profile: {
|
||||
height: 67,
|
||||
marginBottom: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const Member = ({containerStyle, size = 72, showStatus = true, theme, user}: Props) => {
|
||||
const intl = useIntl();
|
||||
const onPress = useCallback(() => {
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-user-profile',
|
||||
icon: closeButton,
|
||||
testID: 'close.settings.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
showModal(screen, title, passProps, options);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={[styles.profile, containerStyle]}
|
||||
type='opacity'
|
||||
>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={size}
|
||||
iconSize={48}
|
||||
showStatus={showStatus}
|
||||
statusSize={24}
|
||||
testID='channel_intro.profile_picture'
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default Member;
|
||||
272
app/screens/channel/intro/illustration/private.tsx
Normal file
272
app/screens/channel/intro/illustration/private.tsx
Normal file
File diff suppressed because one or more lines are too long
358
app/screens/channel/intro/illustration/public.tsx
Normal file
358
app/screens/channel/intro/illustration/public.tsx
Normal file
File diff suppressed because one or more lines are too long
43
app/screens/channel/intro/index.ts
Normal file
43
app/screens/channel/intro/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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 {combineLatest} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@app/constants/database';
|
||||
|
||||
import Intro from './intro';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {CHANNEL, MY_CHANNEL, ROLE, SYSTEM, USER}} = MM_TABLES;
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => {
|
||||
const channel = database.get<ChannelModel>(CHANNEL).findAndObserve(channelId);
|
||||
const myChannel = database.get<MyChannelModel>(MY_CHANNEL).findAndObserve(channelId);
|
||||
const me = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap(({value}) => database.get<UserModel>(USER).findAndObserve(value)),
|
||||
);
|
||||
|
||||
const roles = combineLatest([me, myChannel]).pipe(
|
||||
switchMap(([{roles: userRoles}, {roles: memberRoles}]) => {
|
||||
const combinedRoles = userRoles.split(' ').concat(memberRoles.split(' '));
|
||||
return database.get<RoleModel>(ROLE).query(Q.where('name', Q.oneOf(combinedRoles))).observe();
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
channel,
|
||||
roles,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Intro));
|
||||
86
app/screens/channel/intro/intro.tsx
Normal file
86
app/screens/channel/intro/intro.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {ActivityIndicator, Platform, StyleSheet, View} from 'react-native';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
import DirectChannel from './direct_channel';
|
||||
import PublicOrPrivateChannel from './public_or_private_channel';
|
||||
import TownSquare from './townsquare';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
loading?: boolean;
|
||||
roles: RoleModel[];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginVertical: 12,
|
||||
overflow: 'hidden',
|
||||
...Platform.select({
|
||||
android: {
|
||||
scaleY: -1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const Intro = ({channel, loading = false, roles}: Props) => {
|
||||
const theme = useTheme();
|
||||
const element = useMemo(() => {
|
||||
if (channel.type === General.OPEN_CHANNEL && channel.name === General.DEFAULT_CHANNEL) {
|
||||
return (
|
||||
<TownSquare
|
||||
channelId={channel.id}
|
||||
displayName={channel.displayName}
|
||||
roles={roles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (channel.type) {
|
||||
case General.OPEN_CHANNEL:
|
||||
case General.PRIVATE_CHANNEL:
|
||||
return (
|
||||
<PublicOrPrivateChannel
|
||||
channel={channel}
|
||||
roles={roles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DirectChannel
|
||||
channel={channel}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [channel, roles, theme]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
size='small'
|
||||
color={theme.centerChannelColor}
|
||||
style={styles.container}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{element}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Intro;
|
||||
38
app/screens/channel/intro/options/favorite/favorite.tsx
Normal file
38
app/screens/channel/intro/options/favorite/favorite.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {saveFavoriteChannel} from '@actions/remote/preference';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
import OptionItem from '../item';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
isFavorite: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const IntroFavorite = ({channelId, isFavorite, theme}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const toggleFavorite = useCallback(() => {
|
||||
saveFavoriteChannel(serverUrl, channelId, !isFavorite);
|
||||
}, [channelId, isFavorite]);
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
applyMargin={true}
|
||||
color={isFavorite ? theme.buttonBg : undefined}
|
||||
iconName={isFavorite ? 'star' : 'star-outline'}
|
||||
label={formatMessage({id: 'intro.favorite', defaultMessage: 'Favorite'})}
|
||||
onPress={toggleFavorite}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroFavorite;
|
||||
29
app/screens/channel/intro/options/favorite/index.ts
Normal file
29
app/screens/channel/intro/options/favorite/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 {Preferences} from '@constants';
|
||||
|
||||
import FavoriteItem from './favorite';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
||||
const enhanced = withObservables([], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => ({
|
||||
isFavorite: database.get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).query(
|
||||
Q.where('category', Preferences.CATEGORY_FAVORITE_CHANNEL),
|
||||
Q.where('name', channelId),
|
||||
).observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => {
|
||||
return prefs.length ? of$(prefs[0].value === 'true') : of$(false);
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(FavoriteItem));
|
||||
86
app/screens/channel/intro/options/index.tsx
Normal file
86
app/screens/channel/intro/options/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {showModal} from '@screens/navigation';
|
||||
|
||||
import IntroFavorite from './favorite';
|
||||
import OptionItem from './item';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
header?: boolean;
|
||||
favorite?: boolean;
|
||||
people?: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
marginTop: 28,
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const IntroOptions = ({channelId, header, favorite, people, theme}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const onAddPeople = useCallback(() => {
|
||||
const title = formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'});
|
||||
showModal(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
}, []);
|
||||
|
||||
const onSetHeader = useCallback(() => {
|
||||
const title = formatMessage({id: 'screens.channel_edit', defaultMessage: 'Edit Channel'});
|
||||
showModal(Screens.CHANNEL_EDIT, title, {channelId});
|
||||
}, []);
|
||||
|
||||
const onDetails = useCallback(() => {
|
||||
const title = formatMessage({id: 'screens.channel_details', defaultMessage: 'Channel Details'});
|
||||
showModal(Screens.CHANNEL_DETAILS, title, {channelId});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{people &&
|
||||
<OptionItem
|
||||
applyMargin={true}
|
||||
iconName='account-plus-outline'
|
||||
label={formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'})}
|
||||
onPress={onAddPeople}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{header &&
|
||||
<OptionItem
|
||||
applyMargin={true}
|
||||
iconName='pencil-outline'
|
||||
label={formatMessage({id: 'intro.set_header', defaultMessage: 'Set Header'})}
|
||||
onPress={onSetHeader}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{favorite &&
|
||||
<IntroFavorite
|
||||
channelId={channelId}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
<OptionItem
|
||||
iconName='information-outline'
|
||||
label={formatMessage({id: 'intro.channel_details', defaultMessage: 'Details'})}
|
||||
onPress={onDetails}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroOptions;
|
||||
79
app/screens/channel/intro/options/item.tsx
Normal file
79
app/screens/channel/intro/options/item.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {Pressable, PressableStateCallbackType, Text} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
applyMargin?: boolean;
|
||||
color?: string;
|
||||
iconName: string;
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
borderRadius: 4,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
width: 112,
|
||||
},
|
||||
containerPressed: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
},
|
||||
label: {
|
||||
marginTop: 6,
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
margin: {
|
||||
marginRight: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
const IntroItem = ({applyMargin, color, iconName, label, onPress, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const pressedStyle = useCallback(({pressed}: PressableStateCallbackType) => {
|
||||
const style = [styles.container];
|
||||
if (pressed) {
|
||||
style.push(styles.containerPressed);
|
||||
}
|
||||
|
||||
if (applyMargin) {
|
||||
style.push(styles.margin);
|
||||
}
|
||||
|
||||
return style;
|
||||
}, [applyMargin, theme]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={pressedStyle}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
color={pressed ? theme.linkColor : color || changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
size={24}
|
||||
/>
|
||||
<Text style={[styles.label, {color: pressed ? theme.linkColor : color || changeOpacity(theme.centerChannelColor, 0.56)}]}>
|
||||
{label}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntroItem;
|
||||
51
app/screens/channel/intro/public_or_private_channel/index.ts
Normal file
51
app/screens/channel/intro/public_or_private_channel/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 {combineLatest, of as of$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import PublicOrPrivateChannel from './public_or_private_channel';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {PREFERENCE, SYSTEM, USER}} = MM_TABLES;
|
||||
|
||||
const enhanced = withObservables([], ({channel, database}: {channel: ChannelModel} & WithDatabaseArgs) => {
|
||||
let creator;
|
||||
if (channel.creatorId) {
|
||||
const config = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(switchMap(({value}) => of$(value as ClientConfig)));
|
||||
const license = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.LICENSE).pipe(switchMap(({value}) => of$(value as ClientLicense)));
|
||||
const preferences = database.get<PreferenceModel>(PREFERENCE).query(Q.where('category', Preferences.CATEGORY_DISPLAY_SETTINGS)).observe();
|
||||
const me = database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap(({value}) => database.get<UserModel>(USER).findAndObserve(value)),
|
||||
);
|
||||
|
||||
const profile = channel.creator.observe();
|
||||
const teammateNameDisplay = combineLatest([preferences, config, license]).pipe(
|
||||
map(([prefs, cfg, lcs]) => getTeammateNameDisplaySetting(prefs, cfg, lcs)),
|
||||
);
|
||||
creator = combineLatest([profile, teammateNameDisplay, me]).pipe(
|
||||
map(([user, displaySetting, currentUser]) => (user ? displayUsername(user as UserModel, currentUser.locale, displaySetting, true) : '')),
|
||||
);
|
||||
} else {
|
||||
creator = of$(undefined);
|
||||
}
|
||||
|
||||
return {
|
||||
creator,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(PublicOrPrivateChannel));
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {fetchChannelCreator} from '@actions/remote/channel';
|
||||
import CompassIcon from '@app/components/compass_icon';
|
||||
import {General, Permissions} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
import {hasPermission} from '@utils/role';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import PrivateChannel from '../illustration/private';
|
||||
import PublicChannel from '../illustration/public';
|
||||
import IntroOptions from '../options';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
creator?: string;
|
||||
roles: RoleModel[];
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
created: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 50, 'Regular'),
|
||||
},
|
||||
icon: {
|
||||
marginRight: 5,
|
||||
},
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
textAlign: 'center',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const illustration = useMemo(() => {
|
||||
if (channel.type === General.OPEN_CHANNEL) {
|
||||
return <PublicChannel theme={theme}/>;
|
||||
}
|
||||
|
||||
return <PrivateChannel theme={theme}/>;
|
||||
}, [channel.type, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!creator && channel.creatorId) {
|
||||
fetchChannelCreator(serverUrl, channel.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const canManagePeople = useMemo(() => {
|
||||
const permission = channel.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS : Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS;
|
||||
return hasPermission(roles, permission, false);
|
||||
}, [channel.type, roles]);
|
||||
|
||||
const canSetHeader = useMemo(() => {
|
||||
const permission = channel.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES : Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES;
|
||||
return hasPermission(roles, permission, false);
|
||||
}, [channel.type, roles]);
|
||||
|
||||
const createdBy = useMemo(() => {
|
||||
const id = channel.type === General.OPEN_CHANNEL ? t('intro.public_channel') : t('intro.private_channel');
|
||||
const defaultMessage = channel.type === General.OPEN_CHANNEL ? 'Public Channel' : 'Private Channel';
|
||||
const channelType = `${intl.formatMessage({id, defaultMessage})} `;
|
||||
|
||||
const date = intl.formatDate(channel.createAt, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const by = intl.formatMessage({id: 'intro.created_by', defaultMessage: 'created by {creator} on {date}.'}, {
|
||||
creator,
|
||||
date,
|
||||
});
|
||||
|
||||
return `${channelType} ${by}`;
|
||||
}, [channel.type, creator, theme]);
|
||||
|
||||
const message = useMemo(() => {
|
||||
const id = channel.type === General.OPEN_CHANNEL ? t('intro.welcome.public') : t('intro.welcome.private');
|
||||
const msg = channel.type === General.OPEN_CHANNEL ? 'Add some more team members to the channel or start a conversation below.' : 'Only invited members can see messages posted in this private channel.';
|
||||
const mainMessage = intl.formatMessage({
|
||||
id: 'intro.welcome',
|
||||
defaultMessage: 'Welcome to {displayName} channel.',
|
||||
}, {displayName: channel.displayName});
|
||||
|
||||
const suffix = intl.formatMessage({id, defaultMessage: msg});
|
||||
|
||||
return `${mainMessage} ${suffix}`;
|
||||
}, [channel.displayName, channel.type, theme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{illustration}
|
||||
<Text style={styles.title}>
|
||||
{channel.displayName}
|
||||
</Text>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<CompassIcon
|
||||
name={channel.type === General.OPEN_CHANNEL ? 'globe' : 'lock'}
|
||||
size={14.4}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.created}>
|
||||
{createdBy}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.message}>
|
||||
{message}
|
||||
</Text>
|
||||
<IntroOptions
|
||||
channelId={channel.id}
|
||||
header={canSetHeader}
|
||||
people={canManagePeople}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicOrPrivateChannel;
|
||||
66
app/screens/channel/intro/townsquare/index.tsx
Normal file
66
app/screens/channel/intro/townsquare/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Permissions} from '@constants';
|
||||
import {hasPermission} from '@utils/role';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import PublicChannel from '../illustration/public';
|
||||
import IntroOptions from '../options';
|
||||
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
displayName: string;
|
||||
roles: RoleModel[];
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
textAlign: 'center',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
width: '100%',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const TownSquare = ({channelId, displayName, roles, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<PublicChannel theme={theme}/>
|
||||
<Text style={styles.title}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<FormattedText
|
||||
defaultMessage='Welcome to {name}. Everyone automatically becomes a member of this channel when they join the team.'
|
||||
id='intro.townsquare'
|
||||
style={styles.message}
|
||||
values={{name: displayName}}
|
||||
/>
|
||||
<IntroOptions
|
||||
channelId={channelId}
|
||||
header={hasPermission(roles, Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES, false)}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TownSquare;
|
||||
@@ -9,7 +9,6 @@
|
||||
"about.teamEditionSt": "All your team communication in one place, instantly searchable and accessible anywhere.",
|
||||
"about.teamEditiont0": "Team Edition",
|
||||
"about.teamEditiont1": "Enterprise Edition",
|
||||
"about.title": "About {appTitle}",
|
||||
"account.logout": "Log out",
|
||||
"account.logout_from": "Log out of {serverName}",
|
||||
"account.saved_messages": "Saved Messages",
|
||||
@@ -109,6 +108,19 @@
|
||||
"failed_action.fetch_teams": "An error ocurred while loading the teams of this server",
|
||||
"failed_action.something_wrong": "Something went wrong",
|
||||
"failed_action.try_again": "Try again",
|
||||
"intro.add_people": "Add People",
|
||||
"intro.channel_details": "Details",
|
||||
"intro.created_by": "created by {creator} on {date}.",
|
||||
"intro.direct_message": "This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.",
|
||||
"intro.favorite": "Favorite",
|
||||
"intro.group_message": "This is the start of your conversation with this group. Messages and files shared here are not shown to anyone else outside of the group.",
|
||||
"intro.private_channel": "Private Channel",
|
||||
"intro.public_channel": "Public Channel",
|
||||
"intro.set_header": "Set Header",
|
||||
"intro.townsquare": "Welcome to {name}. Everyone automatically becomes a member of this channel when they join the team.",
|
||||
"intro.welcome": "Welcome to {displayName} channel.",
|
||||
"intro.welcome.private": "Only invited members can see messages posted in this private channel.",
|
||||
"intro.welcome.public": "Add some more team members to the channel or start a conversation below.",
|
||||
"last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.",
|
||||
"last_users_message.added_to_team.type": "were **added to the team** by {actor}.",
|
||||
"last_users_message.first": "{firstUser} and ",
|
||||
@@ -299,6 +311,8 @@
|
||||
"screen.mentions.subtitle": "Messages you've been mentioned in",
|
||||
"screen.mentions.title": "Recent Mentions",
|
||||
"screen.search.placeholder": "Search messages & files",
|
||||
"screens.channel_details": "Channel Details",
|
||||
"screens.channel_edit": "Edit Channel",
|
||||
"search_bar.search": "Search",
|
||||
"status_dropdown.set_away": "Away",
|
||||
"status_dropdown.set_dnd": "Do Not Disturb",
|
||||
|
||||
5
types/api/channels.d.ts
vendored
5
types/api/channels.d.ts
vendored
@@ -41,6 +41,11 @@ type ChannelWithTeamData = Channel & {
|
||||
team_name: string;
|
||||
team_update_at: number;
|
||||
}
|
||||
type ChannelMember = {
|
||||
id?: string;
|
||||
channel_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
type ChannelMembership = {
|
||||
id?: string;
|
||||
channel_id: string;
|
||||
|
||||
2
types/database/database.d.ts
vendored
2
types/database/database.d.ts
vendored
@@ -222,7 +222,7 @@ export type HandleGroupArgs = PrepareOnly & {
|
||||
};
|
||||
|
||||
export type HandleChannelMembershipArgs = PrepareOnly & {
|
||||
channelMemberships: ChannelMembership[];
|
||||
channelMemberships: ChannelMember[];
|
||||
};
|
||||
|
||||
export type HandleGroupMembershipArgs = PrepareOnly & {
|
||||
|
||||
1
types/database/raw_values.d.ts
vendored
1
types/database/raw_values.d.ts
vendored
@@ -97,6 +97,7 @@ type RawValue =
|
||||
| AppInfo
|
||||
| Channel
|
||||
| ChannelInfo
|
||||
| ChannelMember
|
||||
| ChannelMembership
|
||||
| CustomEmoji
|
||||
| Draft
|
||||
|
||||
Reference in New Issue
Block a user