forked from Ivasoft/mattermost-mobile
[Gekidou] Channel Info screen (#6330)
* Channel Info screen * Delete the channel & related data when archiving while viewing archived channels is off * feedback review * UX feedback * Add missing isOptionItem prop
This commit is contained in:
@@ -6,7 +6,7 @@ import {Model} from '@nozbe/watermelondb';
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {removeCurrentUserFromChannel, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -1234,3 +1234,97 @@ export const toggleMuteChannel = async (serverUrl: string, channelId: string, sh
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const archiveChannel = async (serverUrl: string, channelId: string) => {
|
||||
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 {database} = operator;
|
||||
const config = await getConfig(database);
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.deleteChannel(channelId);
|
||||
if (config?.ExperimentalViewArchivedChannels === 'true') {
|
||||
await setChannelDeleteAt(serverUrl, channelId, Date.now());
|
||||
} else {
|
||||
removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeArchivingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const unarchiveChannel = async (serverUrl: string, channelId: string) => {
|
||||
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 {
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.unarchiveChannel(channelId);
|
||||
await setChannelDeleteAt(serverUrl, channelId, Date.now());
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeArchivingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const convertChannelToPrivate = async (serverUrl: string, channelId: string) => {
|
||||
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 {database} = operator;
|
||||
const channel = await getChannelById(database, channelId);
|
||||
if (channel) {
|
||||
EphemeralStore.addConvertingChannel(channelId);
|
||||
}
|
||||
await client.convertChannelToPrivate(channelId);
|
||||
if (channel) {
|
||||
channel.prepareUpdate((c) => {
|
||||
c.type = General.PRIVATE_CHANNEL;
|
||||
});
|
||||
await operator.batchRecords([channel]);
|
||||
}
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.removeConvertingChannel(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,6 +68,10 @@ export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
|
||||
|
||||
export async function handleChannelUnarchiveEvent(serverUrl: string, msg: any) {
|
||||
try {
|
||||
if (EphemeralStore.isArchivingChannel(msg.data.channel_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setChannelDeleteAt(serverUrl, msg.data.channel_id, 0);
|
||||
} catch {
|
||||
// do nothing
|
||||
@@ -82,6 +86,10 @@ export async function handleChannelConvertedEvent(serverUrl: string, msg: any) {
|
||||
|
||||
try {
|
||||
const channelId = msg.data.channel_id;
|
||||
if (EphemeralStore.isConvertingChannel(channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {channel} = await fetchChannelById(serverUrl, channelId);
|
||||
if (channel) {
|
||||
operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
@@ -394,7 +402,7 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocke
|
||||
try {
|
||||
const {database} = operator;
|
||||
const {channel_id: channelId, delete_at: deleteAt} = msg.data;
|
||||
if (EphemeralStore.isLeavingChannel(channelId)) {
|
||||
if (EphemeralStore.isLeavingChannel(channelId) || EphemeralStore.isArchivingChannel(channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,27 @@ import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inModal?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const AddPeopleBox = ({channelId, containerStyle, testID}: Props) => {
|
||||
const AddPeopleBox = ({channelId, containerStyle, inModal, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onAddPeople = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = intl.formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'});
|
||||
if (inModal) {
|
||||
goToScreen(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
return;
|
||||
}
|
||||
await dismissBottomSheet();
|
||||
showModal(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
}, [intl, channelId]);
|
||||
}, [intl, channelId, inModal]);
|
||||
|
||||
return (
|
||||
<OptionBox
|
||||
|
||||
78
app/components/channel_actions/channel_actions.tsx
Normal file
78
app/components/channel_actions/channel_actions.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import CopyChannelLinkBox from '@components/channel_actions/copy_channel_link_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import MutedBox from '@components/channel_actions/mute_box';
|
||||
import SetHeaderBox from '@components/channel_actions/set_header_box';
|
||||
import {General} from '@constants';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
channelType?: string;
|
||||
inModal?: boolean;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT = 62;
|
||||
const DIRECT_CHANNELS: string[] = [General.DM_CHANNEL, General.GM_CHANNEL];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
height: OPTIONS_HEIGHT,
|
||||
},
|
||||
separator: {
|
||||
width: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const ChannelActions = ({channelId, channelType, inModal = false}: Props) => {
|
||||
const onCopyLinkAnimationEnd = useCallback(() => {
|
||||
if (!inModal) {
|
||||
requestAnimationFrame(async () => {
|
||||
await dismissBottomSheet();
|
||||
});
|
||||
}
|
||||
}, [inModal]);
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<FavoriteBox
|
||||
channelId={channelId}
|
||||
showSnackBar={!inModal}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<MutedBox
|
||||
channelId={channelId}
|
||||
showSnackBar={!inModal}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{channelType && DIRECT_CHANNELS.includes(channelType) &&
|
||||
<SetHeaderBox
|
||||
channelId={channelId}
|
||||
inModal={inModal}
|
||||
/>
|
||||
}
|
||||
{channelType && !DIRECT_CHANNELS.includes(channelType) &&
|
||||
<>
|
||||
<AddPeopleBox
|
||||
channelId={channelId}
|
||||
inModal={inModal}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<CopyChannelLinkBox
|
||||
channelId={channelId}
|
||||
onAnimationEnd={onCopyLinkAnimationEnd}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelActions;
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// 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';
|
||||
@@ -11,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import ChannelQuickAction from './quick_actions';
|
||||
import ChannelActions from './channel_actions';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
@@ -29,4 +26,4 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelQuickAction));
|
||||
export default withDatabase(enhanced(ChannelActions));
|
||||
@@ -5,9 +5,11 @@ import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import OptionBox from '@components/option_box';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
@@ -19,12 +21,25 @@ type Props = {
|
||||
|
||||
const InfoBox = ({channelId, containerStyle, showAsLabel = false, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const onViewInfo = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = intl.formatMessage({id: 'screens.channel_info', defaultMessage: 'Channel Info'});
|
||||
showModal(Screens.CHANNEL_INFO, title, {channelId});
|
||||
}, [intl, channelId]);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-channel-info';
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: closeButtonId,
|
||||
}],
|
||||
},
|
||||
};
|
||||
showModal(Screens.CHANNEL_INFO, title, {channelId, closeButtonId}, options);
|
||||
}, [intl, channelId, theme]);
|
||||
|
||||
if (showAsLabel) {
|
||||
return (
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import LeaveChannelLabel from './leave_channel_label';
|
||||
|
||||
@@ -20,7 +22,16 @@ type OwnProps = WithDatabaseArgs & {
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const canLeave = channel.pipe(
|
||||
combineLatestWith(currentUser),
|
||||
switchMap(([ch, u]) => {
|
||||
const isDefaultChannel = ch?.name === General.DEFAULT_CHANNEL;
|
||||
return of$(!isDefaultChannel || (isDefaultChannel && u?.isGuest));
|
||||
}),
|
||||
);
|
||||
|
||||
const displayName = channel.pipe(
|
||||
switchMap((c) => of$(c?.displayName)),
|
||||
);
|
||||
@@ -29,6 +40,7 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps
|
||||
);
|
||||
|
||||
return {
|
||||
canLeave,
|
||||
displayName,
|
||||
type,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {Alert} from 'react-native';
|
||||
|
||||
import {leaveChannel} from '@actions/remote/channel';
|
||||
import {setDirectChannelVisible} from '@actions/remote/preference';
|
||||
import OptionItem from '@components/option_item';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
@@ -14,13 +15,15 @@ import {useIsTablet} from '@hooks/device';
|
||||
import {dismissAllModals, dismissBottomSheet, popToRoot} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
isOptionItem?: boolean;
|
||||
canLeave: boolean;
|
||||
channelId: string;
|
||||
displayName?: string;
|
||||
type?: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const LeaveChanelLabel = ({channelId, displayName, type, testID}: Props) => {
|
||||
const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
@@ -134,27 +137,45 @@ const LeaveChanelLabel = ({channelId, displayName, type, testID}: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!displayName || !type) {
|
||||
if (!displayName || !type || !canLeave) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let leaveText = '';
|
||||
let leaveText;
|
||||
let icon;
|
||||
switch (type) {
|
||||
case General.DM_CHANNEL:
|
||||
leaveText = intl.formatMessage({id: 'channel_info.close_dm', defaultMessage: 'Close direct message'});
|
||||
icon = 'close';
|
||||
break;
|
||||
case General.GM_CHANNEL:
|
||||
leaveText = intl.formatMessage({id: 'channel_info.close_gm', defaultMessage: 'Close group message'});
|
||||
icon = 'close';
|
||||
break;
|
||||
default:
|
||||
leaveText = intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave channel'});
|
||||
icon = 'exit-to-app';
|
||||
break;
|
||||
}
|
||||
|
||||
if (isOptionItem) {
|
||||
return (
|
||||
<OptionItem
|
||||
action={onLeave}
|
||||
destructive={true}
|
||||
icon={icon}
|
||||
|
||||
label={leaveText}
|
||||
testID={testID}
|
||||
type='default'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlideUpPanelItem
|
||||
destructive={true}
|
||||
icon='close'
|
||||
icon={icon}
|
||||
onPress={onLeave}
|
||||
text={leaveText}
|
||||
testID={testID}
|
||||
|
||||
@@ -7,21 +7,27 @@ import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
isHeaderSet: boolean;
|
||||
inModal?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const SetHeaderBox = ({channelId, containerStyle, isHeaderSet, testID}: Props) => {
|
||||
const SetHeaderBox = ({channelId, containerStyle, isHeaderSet, inModal, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onSetHeader = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = intl.formatMessage({id: 'screens.channel_edit_header', defaultMessage: 'Edit Channel Header'});
|
||||
if (inModal) {
|
||||
goToScreen(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId, headerOnly: true});
|
||||
return;
|
||||
}
|
||||
|
||||
await dismissBottomSheet();
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId, headerOnly: true});
|
||||
}, [intl, channelId]);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import FormattedTime from '@components/formatted_time';
|
||||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {getCurrentMomentForTimezone} from '@utils/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -61,6 +62,7 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
<FormattedText
|
||||
id='custom_status.expiry_time.today'
|
||||
defaultMessage='Today'
|
||||
style={[styles.text, textStyles]}
|
||||
/>
|
||||
);
|
||||
} else if (expiryMomentTime.isAfter(todayEndTime) && expiryMomentTime.isSameOrBefore(tomorrowEndTime)) {
|
||||
@@ -68,6 +70,7 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
<FormattedText
|
||||
id='custom_status.expiry_time.tomorrow'
|
||||
defaultMessage='Tomorrow'
|
||||
style={[styles.text, textStyles]}
|
||||
/>
|
||||
);
|
||||
} else if (expiryMomentTime.isAfter(tomorrowEndTime)) {
|
||||
@@ -83,11 +86,12 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
format={format}
|
||||
timezone={timezone}
|
||||
value={expiryMomentTime.toDate()}
|
||||
style={[styles.text, textStyles]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const useTime = showTimeCompulsory || !(expiryMomentTime.isSame(todayEndTime) || expiryMomentTime.isAfter(tomorrowEndTime));
|
||||
const useTime = showTimeCompulsory ?? !(expiryMomentTime.isSame(todayEndTime) || expiryMomentTime.isAfter(tomorrowEndTime));
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -99,6 +103,7 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
<FormattedText
|
||||
id='custom_status.expiry.until'
|
||||
defaultMessage='Until'
|
||||
style={[styles.text, textStyles]}
|
||||
/>
|
||||
)}
|
||||
{showPrefix && ' '}
|
||||
@@ -109,6 +114,7 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
<FormattedText
|
||||
id='custom_status.expiry.at'
|
||||
defaultMessage='at'
|
||||
style={[styles.text, textStyles]}
|
||||
/>
|
||||
{' '}
|
||||
</>
|
||||
@@ -118,6 +124,7 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
isMilitaryTime={isMilitaryTime}
|
||||
timezone={timezone || ''}
|
||||
value={expiryMomentTime.toDate()}
|
||||
style={[styles.text, textStyles]}
|
||||
/>
|
||||
)}
|
||||
{withinBrackets && ')'}
|
||||
@@ -126,6 +133,7 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUser: observeCurrentUser(database),
|
||||
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap(
|
||||
|
||||
@@ -132,8 +132,8 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
testID,
|
||||
...props
|
||||
}: FloatingTextInputProps, ref) => {
|
||||
const [focused, setIsFocused] = useState(Boolean(value) && editable);
|
||||
const [focusedLabel, setIsFocusLabel] = useState<boolean | undefined>(Boolean(placeholder || value));
|
||||
const [focused, setIsFocused] = useState(false);
|
||||
const [focusedLabel, setIsFocusLabel] = useState<boolean | undefined>();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const debouncedOnFocusTextInput = debounce(setIsFocusLabel, 500, {leading: true, trailing: false});
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
@@ -41,15 +41,21 @@ type MarkdownProps = {
|
||||
channelMentions?: ChannelMentions;
|
||||
disableAtChannelMentionHighlight?: boolean;
|
||||
disableAtMentions?: boolean;
|
||||
disableBlockQuote?: boolean;
|
||||
disableChannelLink?: boolean;
|
||||
disableCodeBlock?: boolean;
|
||||
disableGallery?: boolean;
|
||||
disableHashtags?: boolean;
|
||||
disableHeading?: boolean;
|
||||
disableQuotes?: boolean;
|
||||
disableTables?: boolean;
|
||||
enableLatex: boolean;
|
||||
enableInlineLatex: boolean;
|
||||
imagesMetadata?: Record<string, PostImage>;
|
||||
isEdited?: boolean;
|
||||
isReplyPost?: boolean;
|
||||
isSearchResult?: boolean;
|
||||
layoutHeight?: number;
|
||||
layoutWidth?: number;
|
||||
location?: string;
|
||||
mentionKeys?: UserMentionKey[];
|
||||
@@ -87,6 +93,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
atMentionOpacity: {
|
||||
opacity: 1,
|
||||
},
|
||||
bold: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -116,9 +125,10 @@ const computeTextStyle = (textStyles: MarkdownTextStyles, baseStyle: StyleProp<T
|
||||
|
||||
const Markdown = ({
|
||||
autolinkedUrlSchemes, baseTextStyle, blockStyles, channelMentions,
|
||||
disableAtChannelMentionHighlight = false, disableAtMentions = false, disableChannelLink = false,
|
||||
disableGallery = false, disableHashtags = false, enableInlineLatex, enableLatex,
|
||||
imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutWidth,
|
||||
disableAtChannelMentionHighlight, disableAtMentions, disableBlockQuote, disableChannelLink,
|
||||
disableCodeBlock, disableGallery, disableHashtags, disableHeading, disableTables,
|
||||
enableInlineLatex, enableLatex,
|
||||
imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutHeight, layoutWidth,
|
||||
location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns,
|
||||
textStyles = {}, theme, value = '',
|
||||
}: MarkdownProps) => {
|
||||
@@ -148,6 +158,10 @@ const Markdown = ({
|
||||
};
|
||||
|
||||
const renderBlockQuote = ({children, ...otherProps}: any) => {
|
||||
if (disableBlockQuote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownBlockQuote
|
||||
iconStyle={blockStyles?.quoteBlockIcon}
|
||||
@@ -191,6 +205,10 @@ const Markdown = ({
|
||||
};
|
||||
|
||||
const renderCodeBlock = (props: any) => {
|
||||
if (disableCodeBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// These sometimes include a trailing newline
|
||||
const content = props.literal.replace(/\n$/, '');
|
||||
|
||||
@@ -264,11 +282,20 @@ const Markdown = ({
|
||||
};
|
||||
|
||||
const renderHeading = ({children, level}: {children: ReactElement; level: string}) => {
|
||||
if (disableHeading) {
|
||||
return (
|
||||
<Text style={style.bold}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const containerStyle = [
|
||||
style.block,
|
||||
textStyles[`heading${level}`],
|
||||
];
|
||||
const textStyle = textStyles[`heading${level}Text`];
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<Text style={textStyle}>
|
||||
@@ -314,6 +341,7 @@ const Markdown = ({
|
||||
<MarkdownImage
|
||||
disabled={disableGallery ?? Boolean(!location)}
|
||||
errorTextStyle={[computeTextStyle(textStyles, baseTextStyle, context), textStyles.error]}
|
||||
layoutHeight={layoutHeight}
|
||||
layoutWidth={layoutWidth}
|
||||
linkDestination={linkDestination}
|
||||
imagesMetadata={imagesMetadata}
|
||||
@@ -396,6 +424,9 @@ const Markdown = ({
|
||||
};
|
||||
|
||||
const renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => {
|
||||
if (disableTables) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MarkdownTable
|
||||
numColumns={numColumns}
|
||||
@@ -425,7 +456,12 @@ const Markdown = ({
|
||||
}
|
||||
|
||||
// Construct the text style based off of the parents of this node since RN's inheritance is limited
|
||||
const styles = computeTextStyle(textStyles, baseTextStyle, context);
|
||||
let styles;
|
||||
if (disableHeading) {
|
||||
styles = computeTextStyle(textStyles, baseTextStyle, context.filter((c) => !c.startsWith('heading')));
|
||||
} else {
|
||||
styles = computeTextStyle(textStyles, baseTextStyle, context);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -497,7 +533,7 @@ const Markdown = ({
|
||||
};
|
||||
|
||||
const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current;
|
||||
const renderer = useMemo(() => createRenderer(), [theme]);
|
||||
const renderer = useMemo(createRenderer, [theme, textStyles]);
|
||||
let ast = parser.parse(value.toString());
|
||||
|
||||
ast = combineTextNodes(ast);
|
||||
|
||||
@@ -37,6 +37,7 @@ type MarkdownImageProps = {
|
||||
imagesMetadata: Record<string, PostImage>;
|
||||
isReplyPost?: boolean;
|
||||
linkDestination?: string;
|
||||
layoutHeight?: number;
|
||||
layoutWidth?: number;
|
||||
location?: string;
|
||||
postId: string;
|
||||
@@ -67,7 +68,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
|
||||
const MarkdownImage = ({
|
||||
disabled, errorTextStyle, imagesMetadata, isReplyPost = false,
|
||||
layoutWidth, linkDestination, location, postId, source, sourceSize,
|
||||
layoutWidth, layoutHeight, linkDestination, location, postId, source, sourceSize,
|
||||
}: MarkdownImageProps) => {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
@@ -78,7 +79,7 @@ const MarkdownImage = ({
|
||||
const genericFileId = useRef(generateId('uid')).current;
|
||||
const metadata = imagesMetadata?.[source] || Object.values(imagesMetadata || {})[0];
|
||||
const [failed, setFailed] = useState(isGifTooLarge(metadata));
|
||||
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth);
|
||||
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight);
|
||||
const serverUrl = useServerUrl();
|
||||
const galleryIdentifier = `${postId}-${genericFileId}-${location}`;
|
||||
const uri = source.startsWith('/') ? serverUrl + source : source;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import Animated, {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -28,12 +28,14 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
maxHeight: OPTIONS_HEIGHT,
|
||||
minWidth: 80,
|
||||
},
|
||||
background: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
},
|
||||
center: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -115,10 +117,11 @@ const AnimatedOptionBox = ({
|
||||
<AnimatedPressable
|
||||
onPress={handleOnPress}
|
||||
disabled={activated}
|
||||
style={styles.container}
|
||||
testID={testID}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<Animated.View style={[styles.container, pressed && {backgroundColor: changeOpacity(theme.buttonBg, 0.16)}]}>
|
||||
<View style={[styles.container, styles.background, pressed && {backgroundColor: changeOpacity(theme.buttonBg, 0.16)}]}>
|
||||
<Animated.View style={[styles.container, backgroundStyle]}>
|
||||
<Animated.View style={[StyleSheet.absoluteFill, styles.center, optionStyle]}>
|
||||
<CompassIcon
|
||||
@@ -149,7 +152,7 @@ const AnimatedOptionBox = ({
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
</AnimatedPressable>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingHorizontal: 5,
|
||||
textTransform: 'capitalize',
|
||||
...typography('Body', 50, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
177
app/components/option_item/index.tsx
Normal file
177
app/components/option_item/index.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {Switch, Text, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
action: (value: string | boolean) => void;
|
||||
description?: string;
|
||||
destructive?: boolean;
|
||||
icon?: string;
|
||||
info?: string;
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
testID?: string;
|
||||
type: OptionType;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const OptionType = {
|
||||
ARROW: 'arrow',
|
||||
DEFAULT: 'default',
|
||||
TOGGLE: 'toggle',
|
||||
SELECT: 'select',
|
||||
} as const;
|
||||
|
||||
type OptionType = typeof OptionType[keyof typeof OptionType];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
actionContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 16,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
minHeight: 48,
|
||||
},
|
||||
destructive: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 75),
|
||||
marginTop: 2,
|
||||
},
|
||||
iconContainer: {marginRight: 16},
|
||||
infoContainer: {marginRight: 2},
|
||||
info: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 100),
|
||||
},
|
||||
label: {
|
||||
flexShrink: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
labelText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const OptionItem = ({
|
||||
action, description, destructive, icon,
|
||||
info, label, selected,
|
||||
testID = 'optionItem', type, value,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let actionComponent;
|
||||
if (type === OptionType.SELECT && selected) {
|
||||
actionComponent = (
|
||||
<CompassIcon
|
||||
color={theme.linkColor}
|
||||
name='check'
|
||||
size={24}
|
||||
testID={`${testID}.selected`}
|
||||
/>
|
||||
);
|
||||
} else if (type === OptionType.TOGGLE) {
|
||||
actionComponent = (
|
||||
<Switch
|
||||
onValueChange={action}
|
||||
value={selected}
|
||||
testID={`${testID}.toggled.${selected}`}
|
||||
/>
|
||||
);
|
||||
} else if (type === OptionType.ARROW) {
|
||||
actionComponent = (
|
||||
<CompassIcon
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
name='chevron-right'
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
action(value || '');
|
||||
}, [value, action]);
|
||||
|
||||
const component = (
|
||||
<View
|
||||
testID={testID}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.labelContainer}>
|
||||
{Boolean(icon) && (
|
||||
<View style={styles.iconContainer}>
|
||||
<CompassIcon
|
||||
name={icon!}
|
||||
size={24}
|
||||
color={destructive ? theme.dndIndicator : changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.label}>
|
||||
<Text
|
||||
style={[styles.labelText, destructive && styles.destructive]}
|
||||
testID={`${testID}.label`}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{Boolean(description) &&
|
||||
<Text
|
||||
style={[styles.description, destructive && styles.destructive]}
|
||||
testID={`${testID}.description`}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{Boolean(actionComponent) &&
|
||||
<View style={styles.actionContainer}>
|
||||
{Boolean(info) &&
|
||||
<View style={styles.infoContainer}>
|
||||
<Text style={styles.info}>{info}</Text>
|
||||
</View>
|
||||
}
|
||||
{actionComponent}
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (type === OptionType.DEFAULT || type === OptionType.SELECT || type === OptionType.ARROW) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
{component}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
export default OptionItem;
|
||||
@@ -58,7 +58,14 @@ const Image = ({author, iconSize, size, source}: Props) => {
|
||||
}
|
||||
|
||||
if (author && client) {
|
||||
const lastPictureUpdate = ('lastPictureUpdate' in author) ? author.lastPictureUpdate : author.last_picture_update;
|
||||
let lastPictureUpdate = 0;
|
||||
const isBot = ('isBot' in author) ? author.isBot : author.is_bot;
|
||||
if (isBot) {
|
||||
lastPictureUpdate = ('isBot' in author) ? author.props?.bot_last_icon_update : author.bot_last_icon_update || 0;
|
||||
} else {
|
||||
lastPictureUpdate = ('lastPictureUpdate' in author) ? author.lastPictureUpdate : author.last_picture_update;
|
||||
}
|
||||
|
||||
const pictureUrl = client.getProfilePictureUrl(author.id, lastPictureUpdate);
|
||||
const imgSource = source ?? {uri: `${serverUrl}${pictureUrl}`};
|
||||
return (
|
||||
|
||||
@@ -23,7 +23,7 @@ type SlideUpPanelProps = {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const ITEM_HEIGHT = 51;
|
||||
export const ITEM_HEIGHT = 48;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
@@ -40,12 +40,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
iconContainer: {
|
||||
height: 50,
|
||||
height: ITEM_HEIGHT,
|
||||
justifyContent: 'center',
|
||||
marginRight: 10,
|
||||
},
|
||||
noIconContainer: {
|
||||
height: 50,
|
||||
height: ITEM_HEIGHT,
|
||||
width: 18,
|
||||
},
|
||||
icon: {
|
||||
@@ -54,7 +54,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
textContainer: {
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
height: 50,
|
||||
height: ITEM_HEIGHT,
|
||||
marginRight: 5,
|
||||
},
|
||||
text: {
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
|
||||
export const MIN_CHANNEL_NAME_LENGTH = 1;
|
||||
export const MAX_CHANNEL_NAME_LENGTH = 64;
|
||||
export const IGNORE_CHANNEL_MENTIONS_ON = 'on';
|
||||
export const IGNORE_CHANNEL_MENTIONS_OFF = 'off';
|
||||
export const IGNORE_CHANNEL_MENTIONS_DEFAULT = 'default';
|
||||
|
||||
export default {
|
||||
IGNORE_CHANNEL_MENTIONS_ON,
|
||||
IGNORE_CHANNEL_MENTIONS_OFF,
|
||||
IGNORE_CHANNEL_MENTIONS_DEFAULT,
|
||||
MAX_CHANNEL_NAME_LENGTH,
|
||||
MIN_CHANNEL_NAME_LENGTH,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import Integrations from './integrations';
|
||||
import List from './list';
|
||||
import Navigation from './navigation';
|
||||
import Network from './network';
|
||||
import NotificationLevel from './notification_level';
|
||||
import Permissions from './permissions';
|
||||
import Post from './post';
|
||||
import PostDraft from './post_draft';
|
||||
@@ -52,6 +53,7 @@ export {
|
||||
List,
|
||||
Navigation,
|
||||
Network,
|
||||
NotificationLevel,
|
||||
Permissions,
|
||||
Post,
|
||||
PostDraft,
|
||||
|
||||
14
app/constants/notification_level.ts
Normal file
14
app/constants/notification_level.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const ALL = 'all';
|
||||
export const DEFAULT = 'default';
|
||||
export const MENTION = 'mention';
|
||||
export const NONE = 'none';
|
||||
|
||||
export default {
|
||||
ALL,
|
||||
DEFAULT,
|
||||
MENTION,
|
||||
NONE,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ export const CHANNEL = 'Channel';
|
||||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CHANNEL_INFO = 'ChannelInfo';
|
||||
export const CHANNEL_MENTION = 'ChannelMention';
|
||||
export const CODE = 'Code';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
|
||||
@@ -33,6 +34,7 @@ export const LOGIN = 'Login';
|
||||
export const MENTIONS = 'Mentions';
|
||||
export const MFA = 'MFA';
|
||||
export const PERMALINK = 'Permalink';
|
||||
export const PINNED_MESSAGES = 'PinnedMessages';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
export const REACTIONS = 'Reactions';
|
||||
export const SAVED_POSTS = 'SavedPosts';
|
||||
@@ -58,8 +60,8 @@ export default {
|
||||
BROWSE_CHANNELS,
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_EDIT,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MENTION,
|
||||
CODE,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_OR_EDIT_CHANNEL,
|
||||
@@ -83,6 +85,7 @@ export default {
|
||||
MENTIONS,
|
||||
MFA,
|
||||
PERMALINK,
|
||||
PINNED_MESSAGES,
|
||||
POST_OPTIONS,
|
||||
REACTIONS,
|
||||
SAVED_POSTS,
|
||||
@@ -120,9 +123,10 @@ export const MODAL_SCREENS_WITHOUT_BACK = [
|
||||
|
||||
export const NOT_READY = [
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MENTION,
|
||||
CREATE_TEAM,
|
||||
INTEGRATION_SELECTOR,
|
||||
INTERACTIVE_DIALOG,
|
||||
PINNED_MESSAGES,
|
||||
USER_PROFILE,
|
||||
];
|
||||
|
||||
@@ -40,13 +40,28 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs):
|
||||
user.roles = raw.roles;
|
||||
user.username = raw.username;
|
||||
user.notifyProps = raw.notify_props;
|
||||
user.props = raw.props || null;
|
||||
user.timezone = raw.timezone || null;
|
||||
user.isBot = raw.is_bot;
|
||||
user.remoteId = raw?.remote_id ?? null;
|
||||
if (raw.status) {
|
||||
user.status = raw.status;
|
||||
}
|
||||
|
||||
if (raw.bot_description) {
|
||||
raw.props = {
|
||||
...raw.props,
|
||||
bot_description: raw.bot_description,
|
||||
};
|
||||
}
|
||||
|
||||
if (raw.bot_last_icon_update) {
|
||||
raw.props = {
|
||||
...raw.props,
|
||||
bot_last_icon_update: raw.bot_last_icon_update,
|
||||
};
|
||||
}
|
||||
|
||||
user.props = raw.props || null;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
||||
@@ -10,21 +10,23 @@ import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
import {Navigation, Screens} from '@constants';
|
||||
import {General, Navigation, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
import {bottomSheet, popTopScreen, showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import OtherMentionsBadge from './other_mentions_badge';
|
||||
import ChannelQuickAction from './quick_actions';
|
||||
import QuickActions from './quick_actions';
|
||||
|
||||
import type {HeaderRightButton} from '@components/navigation_header/header';
|
||||
|
||||
type ChannelProps = {
|
||||
channelId: string;
|
||||
channelType: ChannelType;
|
||||
customStatus?: UserCustomStatus;
|
||||
isCustomStatusExpired: boolean;
|
||||
componentId?: string;
|
||||
@@ -58,7 +60,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
}));
|
||||
|
||||
const ChannelHeader = ({
|
||||
channelId, componentId, customStatus, displayName,
|
||||
channelId, channelType, componentId, customStatus, displayName,
|
||||
isCustomStatusExpired, isOwnDirectMessage, memberCount,
|
||||
searchTerm, teamId,
|
||||
}: ChannelProps) => {
|
||||
@@ -86,10 +88,34 @@ const ChannelHeader = ({
|
||||
popTopScreen(componentId);
|
||||
}, []);
|
||||
|
||||
const onTitlePress = useCallback(() => {
|
||||
const title = intl.formatMessage({id: 'screens.channel_info', defaultMessage: 'Channel Info'});
|
||||
showModal(Screens.CHANNEL_INFO, title, {channelId});
|
||||
}, [channelId, intl]);
|
||||
const onTitlePress = useCallback(preventDoubleTap(() => {
|
||||
let title;
|
||||
switch (channelType) {
|
||||
case General.DM_CHANNEL:
|
||||
title = intl.formatMessage({id: 'screens.channel_info.dm', defaultMessage: 'Direct message info'});
|
||||
break;
|
||||
case General.GM_CHANNEL:
|
||||
title = intl.formatMessage({id: 'screens.channel_info.gm', defaultMessage: 'Group message info'});
|
||||
break;
|
||||
default:
|
||||
title = intl.formatMessage({id: 'screens.channel_info', defaultMessage: 'Channel info'});
|
||||
break;
|
||||
}
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-channel-info';
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: closeButtonId,
|
||||
}],
|
||||
},
|
||||
};
|
||||
showModal(Screens.CHANNEL_INFO, title, {channelId, closeButtonId}, options);
|
||||
}), [channelId, channelType, intl, theme]);
|
||||
|
||||
const onChannelQuickAction = useCallback(() => {
|
||||
if (isTablet) {
|
||||
@@ -98,7 +124,9 @@ const ChannelHeader = ({
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
return <ChannelQuickAction channelId={channelId}/>;
|
||||
return (
|
||||
<QuickActions channelId={channelId}/>
|
||||
);
|
||||
};
|
||||
|
||||
bottomSheet({
|
||||
@@ -108,7 +136,7 @@ const ChannelHeader = ({
|
||||
theme,
|
||||
closeButtonId: 'close-channel-quick-actions',
|
||||
});
|
||||
}, [channelId, isTablet, onTitlePress, theme]);
|
||||
}, [channelId, channelType, isTablet, onTitlePress, theme]);
|
||||
|
||||
const rightButtons: HeaderRightButton[] = useMemo(() => ([{
|
||||
iconName: 'magnify',
|
||||
|
||||
@@ -25,6 +25,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
switchMap((id) => observeChannel(database, id)),
|
||||
);
|
||||
|
||||
const channelType = channel.pipe(switchMap((c) => of$(c?.type)));
|
||||
const channelInfo = channelId.pipe(
|
||||
switchMap((id) => observeChannelInfo(database, id)),
|
||||
);
|
||||
@@ -74,6 +75,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
|
||||
return {
|
||||
channelId,
|
||||
channelType,
|
||||
customStatus,
|
||||
displayName,
|
||||
isCustomStatusExpired,
|
||||
|
||||
53
app/screens/channel/header/quick_actions/index.tsx
Normal file
53
app/screens/channel/header/quick_actions/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ChannelActions from '@components/channel_actions';
|
||||
import InfoBox from '@components/channel_actions/info_box';
|
||||
import LeaveChannelLabel from '@components/channel_actions/leave_channel_label';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
minHeight: 270,
|
||||
},
|
||||
line: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
height: 1,
|
||||
marginVertical: 8,
|
||||
},
|
||||
wrapper: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
separator: {
|
||||
width: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelQuickAction = ({channelId}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
<ChannelActions channelId={channelId}/>
|
||||
</View>
|
||||
<InfoBox
|
||||
channelId={channelId}
|
||||
showAsLabel={true}
|
||||
/>
|
||||
<View style={styles.line}/>
|
||||
<LeaveChannelLabel channelId={channelId}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelQuickAction;
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import CopyChannelLinkBox from '@components/channel_actions/copy_channel_link_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import InfoBox from '@components/channel_actions/info_box';
|
||||
import LeaveChannelLabel from '@components/channel_actions/leave_channel_label';
|
||||
import MutedBox from '@components/channel_actions/mute_box';
|
||||
import SetHeaderBox from '@components/channel_actions/set_header_box';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
channelType?: string;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT = 62;
|
||||
const DIRECT_CHANNELS: string[] = [General.DM_CHANNEL, General.GM_CHANNEL];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 270,
|
||||
},
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
height: OPTIONS_HEIGHT,
|
||||
marginBottom: 8,
|
||||
},
|
||||
separator: {
|
||||
width: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const ChannelQuickAction = ({channelId, channelType}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const onCopyLinkAnimationEnd = useCallback(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
await dismissBottomSheet();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
<FavoriteBox
|
||||
channelId={channelId}
|
||||
showSnackBar={true}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<MutedBox
|
||||
channelId={channelId}
|
||||
showSnackBar={true}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{channelType && DIRECT_CHANNELS.includes(channelType) &&
|
||||
<SetHeaderBox channelId={channelId}/>
|
||||
}
|
||||
{channelType && !DIRECT_CHANNELS.includes(channelType) &&
|
||||
<>
|
||||
<AddPeopleBox channelId={channelId}/>
|
||||
<View style={styles.separator}/>
|
||||
<CopyChannelLinkBox
|
||||
channelId={channelId}
|
||||
onAnimationEnd={onCopyLinkAnimationEnd}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
<InfoBox
|
||||
channelId={channelId}
|
||||
showAsLabel={true}
|
||||
/>
|
||||
<View style={{backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), height: 1, marginVertical: 8}}/>
|
||||
<LeaveChannelLabel channelId={channelId}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelQuickAction;
|
||||
96
app/screens/channel_info/channel_info.tsx
Normal file
96
app/screens/channel_info/channel_info.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {ScrollView, View} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import ChannelActions from '@components/channel_actions';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import DestructiveOptions from './destructive_options';
|
||||
import Extra from './extra';
|
||||
import Options from './options';
|
||||
import Title from './title';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
closeButtonId: string;
|
||||
componentId: string;
|
||||
type?: ChannelType;
|
||||
}
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
content: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
marginVertical: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelInfo = ({channelId, closeButtonId, componentId, type}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
const update = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: {buttonId: string}) => {
|
||||
if (buttonId === closeButtonId) {
|
||||
dismissModal({componentId});
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
|
||||
return () => {
|
||||
update.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
>
|
||||
<ScrollView
|
||||
bounces={true}
|
||||
alwaysBounceVertical={false}
|
||||
contentContainerStyle={styles.content}
|
||||
>
|
||||
<Title
|
||||
channelId={channelId}
|
||||
type={type}
|
||||
/>
|
||||
<ChannelActions
|
||||
channelId={channelId}
|
||||
inModal={true}
|
||||
/>
|
||||
<Extra channelId={channelId}/>
|
||||
<View style={styles.separator}/>
|
||||
<Options
|
||||
channelId={channelId}
|
||||
type={type}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<DestructiveOptions
|
||||
channelId={channelId}
|
||||
componentId={componentId}
|
||||
type={type}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelInfo;
|
||||
137
app/screens/channel_info/destructive_options/archive/archive.tsx
Normal file
137
app/screens/channel_info/destructive_options/archive/archive.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {MessageDescriptor, useIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {archiveChannel, unarchiveChannel} from '@actions/remote/channel';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
import {dismissModal, popToRoot} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
canArchive: boolean;
|
||||
canUnarchive: boolean;
|
||||
canViewArchivedChannels: boolean;
|
||||
channelId: string;
|
||||
componentId: string;
|
||||
displayName: string;
|
||||
type?: ChannelType;
|
||||
}
|
||||
|
||||
const Archive = ({
|
||||
canArchive, canUnarchive, canViewArchivedChannels,
|
||||
channelId, componentId, displayName, type,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const close = async (pop: boolean) => {
|
||||
await dismissModal({componentId});
|
||||
if (pop) {
|
||||
popToRoot();
|
||||
}
|
||||
};
|
||||
|
||||
const alertAndHandleYesAction = (title: MessageDescriptor, message: MessageDescriptor, onPressAction: () => void) => {
|
||||
const {formatMessage} = intl;
|
||||
let term: string;
|
||||
if (type === General.OPEN_CHANNEL) {
|
||||
term = formatMessage({id: 'channel_info.public_channel', defaultMessage: 'Public Channel'});
|
||||
} else {
|
||||
term = formatMessage({id: 'channel_info.private_channel', defaultMessage: 'Private Channel'});
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
formatMessage(title, {term}),
|
||||
formatMessage(message, {term: term.toLowerCase(), name: displayName}),
|
||||
[{
|
||||
text: formatMessage({id: 'channel_info.alertNo', defaultMessage: 'No'}),
|
||||
}, {
|
||||
text: formatMessage({id: 'channel_info.alertYes', defaultMessage: 'Yes'}),
|
||||
onPress: onPressAction,
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
const onArchive = preventDoubleTap(() => {
|
||||
const title = {id: t('channel_info.archive_title'), defaultMessage: 'Archive {term}'};
|
||||
const message = {
|
||||
id: t('channel_info.archive_description'),
|
||||
defaultMessage: 'Are you sure you want to archive the {term} {name}?',
|
||||
};
|
||||
const onPressAction = async () => {
|
||||
const result = await archiveChannel(serverUrl, channelId);
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(
|
||||
intl,
|
||||
result.error,
|
||||
{
|
||||
id: t('channel_info.archive_failed'),
|
||||
defaultMessage: 'An error occurred trying to archive the channel {displayName}',
|
||||
}, {displayName},
|
||||
);
|
||||
} else {
|
||||
close(!canViewArchivedChannels);
|
||||
}
|
||||
};
|
||||
alertAndHandleYesAction(title, message, onPressAction);
|
||||
});
|
||||
|
||||
const onUnarchive = preventDoubleTap(() => {
|
||||
const title = {id: t('channel_info.unarchive_title'), defaultMessage: 'Unarchive {term}'};
|
||||
const message = {
|
||||
id: t('channel_info.unarchive_description'),
|
||||
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
|
||||
};
|
||||
const onPressAction = async () => {
|
||||
const result = await unarchiveChannel(serverUrl, channelId);
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(
|
||||
intl,
|
||||
result.error,
|
||||
{
|
||||
id: t('channel_info.unarchive_failed'),
|
||||
defaultMessage: 'An error occurred trying to unarchive the channel {displayName}',
|
||||
}, {displayName},
|
||||
);
|
||||
} else {
|
||||
close(false);
|
||||
}
|
||||
};
|
||||
alertAndHandleYesAction(title, message, onPressAction);
|
||||
});
|
||||
|
||||
if (!canArchive && !canUnarchive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canUnarchive) {
|
||||
return (
|
||||
<OptionItem
|
||||
action={onUnarchive}
|
||||
label={intl.formatMessage({id: 'channel_info.unarchive', defaultMessage: 'Unarchive Channel'})}
|
||||
icon='archive-arrow-up-outline'
|
||||
destructive={true}
|
||||
type='default'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={onArchive}
|
||||
label={intl.formatMessage({id: 'channel_info.archive', defaultMessage: 'Archive Channel'})}
|
||||
icon='archive-outline'
|
||||
destructive={true}
|
||||
type='default'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Archive;
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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 {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observePermissionForChannel, observePermissionForTeam} from '@queries/servers/role';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeCurrentTeam} from '@queries/servers/team';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import Archive from './archive';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId', 'type'], ({channelId, database, type}: Props) => {
|
||||
const team = observeCurrentTeam(database);
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const canViewArchivedChannels = observeConfigBooleanValue(database, 'ExperimentalViewArchivedChannels');
|
||||
const isArchived = channel.pipe(switchMap((c) => of$((c?.deleteAt || 0) > 0)));
|
||||
const canLeave = channel.pipe(
|
||||
combineLatestWith(currentUser),
|
||||
switchMap(([ch, u]) => {
|
||||
const isDefaultChannel = ch?.name === General.DEFAULT_CHANNEL;
|
||||
return of$(!isDefaultChannel || (isDefaultChannel && u?.isGuest));
|
||||
}),
|
||||
);
|
||||
|
||||
const canArchive = channel.pipe(
|
||||
combineLatestWith(currentUser, canLeave, isArchived),
|
||||
switchMap(([ch, u, leave, archived]) => {
|
||||
if (
|
||||
type === General.DM_CHANNEL || type === General.GM_CHANNEL ||
|
||||
!ch || !u || !leave || archived
|
||||
) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
if (type === General.OPEN_CHANNEL) {
|
||||
return observePermissionForChannel(ch, u, Permissions.DELETE_PUBLIC_CHANNEL, true);
|
||||
}
|
||||
|
||||
return observePermissionForChannel(ch, u, Permissions.DELETE_PRIVATE_CHANNEL, true);
|
||||
}),
|
||||
);
|
||||
|
||||
const canUnarchive = team.pipe(
|
||||
combineLatestWith(currentUser, isArchived),
|
||||
switchMap(([t, u, archived]) => {
|
||||
if (
|
||||
type === General.DM_CHANNEL || type === General.GM_CHANNEL ||
|
||||
!t || !u || !archived
|
||||
) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
return observePermissionForTeam(t, u, Permissions.MANAGE_TEAM, false);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
canArchive,
|
||||
canUnarchive,
|
||||
canViewArchivedChannels,
|
||||
displayName: channel.pipe(switchMap((c) => of$(c?.displayName))),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Archive));
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {convertChannelToPrivate} from '@actions/remote/channel';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
canConvert: boolean;
|
||||
channelId: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const ConvertPrivate = ({canConvert, channelId, displayName}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onConfirmConvertToPrivate = () => {
|
||||
const {formatMessage} = intl;
|
||||
const title = {id: t('channel_info.convert_private_title'), defaultMessage: 'Convert {displayName} to a private channel?'};
|
||||
const message = {
|
||||
id: t('channel_info.convert_private_description'),
|
||||
defaultMessage: 'When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?',
|
||||
};
|
||||
|
||||
Alert.alert(
|
||||
formatMessage(title, {displayName}),
|
||||
formatMessage(message, {displayName}),
|
||||
[{
|
||||
text: formatMessage({id: 'channel_info.alertNo', defaultMessage: 'No'}),
|
||||
}, {
|
||||
text: formatMessage({id: 'channel_info.alertYes', defaultMessage: 'Yes'}),
|
||||
onPress: convertToPrivate,
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
const convertToPrivate = preventDoubleTap(async () => {
|
||||
const result = await convertChannelToPrivate(serverUrl, channelId);
|
||||
const {formatMessage} = intl;
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(
|
||||
intl,
|
||||
result.error,
|
||||
{
|
||||
id: t('channel_info.convert_failed'),
|
||||
defaultMessage: 'We were unable to convert {displayName} to a private channel.',
|
||||
}, {displayName},
|
||||
[{
|
||||
text: formatMessage({id: 'channel_info.error_close', defaultMessage: 'Close'}),
|
||||
}, {
|
||||
text: formatMessage({id: 'channel_info.alert_retry', defaultMessage: 'Try Again'}),
|
||||
onPress: convertToPrivate,
|
||||
}],
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'',
|
||||
formatMessage({id: t('channel_info.convert_private_success'), defaultMessage: '{displayName} is now a private channel.'}, {displayName}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!canConvert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={onConfirmConvertToPrivate}
|
||||
label={intl.formatMessage({id: 'channel_info.convert_private', defaultMessage: 'Convert to private channel'})}
|
||||
icon='lock-outline'
|
||||
type='default'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertPrivate;
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observePermissionForChannel} from '@queries/servers/role';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import ConvertPrivate from './convert_private';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const canConvert = channel.pipe(
|
||||
combineLatestWith(currentUser),
|
||||
switchMap(([ch, u]) => {
|
||||
if (!ch || !u || ch.name === General.DEFAULT_CHANNEL) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
return observePermissionForChannel(ch, u, Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE, false);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
canConvert,
|
||||
displayName: channel.pipe(switchMap((c) => of$(c?.displayName))),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ConvertPrivate));
|
||||
39
app/screens/channel_info/destructive_options/index.tsx
Normal file
39
app/screens/channel_info/destructive_options/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import LeaveChannelLabel from '@components/channel_actions/leave_channel_label';
|
||||
import {General} from '@constants';
|
||||
|
||||
import Archive from './archive';
|
||||
import ConvertPrivate from './convert_private';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
componentId: string;
|
||||
type?: ChannelType;
|
||||
}
|
||||
|
||||
const DestructiveOptions = ({channelId, componentId, type}: Props) => {
|
||||
return (
|
||||
<>
|
||||
{type === General.OPEN_CHANNEL &&
|
||||
<ConvertPrivate channelId={channelId}/>
|
||||
}
|
||||
<LeaveChannelLabel
|
||||
channelId={channelId}
|
||||
isOptionItem={true}
|
||||
/>
|
||||
{type !== General.DM_CHANNEL && type !== General.GM_CHANNEL &&
|
||||
<Archive
|
||||
channelId={channelId}
|
||||
componentId={componentId}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DestructiveOptions;
|
||||
166
app/screens/channel_info/extra/extra.tsx
Normal file
166
app/screens/channel_info/extra/extra.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment';
|
||||
import React, {useMemo} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedDate from '@components/formatted_date';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Markdown from '@components/markdown';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
createdAt: number;
|
||||
createdBy: string;
|
||||
customStatus?: UserCustomStatus;
|
||||
header?: string;
|
||||
}
|
||||
|
||||
const headerMetadata = {header: {width: 1, height: 1}};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
item: {
|
||||
marginTop: 16,
|
||||
},
|
||||
extraHeading: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
marginBottom: 8,
|
||||
...typography('Body', 75),
|
||||
},
|
||||
header: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200),
|
||||
fontWeight: undefined,
|
||||
},
|
||||
created: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.48),
|
||||
...typography('Body', 75),
|
||||
},
|
||||
customStatus: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
customStatusEmoji: {
|
||||
marginRight: 10,
|
||||
},
|
||||
customStatusLabel: {
|
||||
color: theme.centerChannelColor,
|
||||
marginRight: 8,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
customStatusExpiry: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 75),
|
||||
},
|
||||
}));
|
||||
|
||||
const Extra = ({createdAt, createdBy, customStatus, header}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
const created = useMemo(() => ({
|
||||
user: createdBy,
|
||||
date: (
|
||||
<FormattedDate
|
||||
style={styles.created}
|
||||
value={createdAt}
|
||||
/>
|
||||
),
|
||||
}), [createdAt, createdBy, theme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{Boolean(customStatus) &&
|
||||
<View style={styles.item}>
|
||||
<FormattedText
|
||||
id='channel_info.custom_status'
|
||||
defaultMessage='Custom status:'
|
||||
style={styles.extraHeading}
|
||||
/>
|
||||
<View style={styles.customStatus}>
|
||||
{Boolean(customStatus?.emoji) &&
|
||||
<View style={styles.customStatusEmoji}>
|
||||
<Emoji
|
||||
emojiName={customStatus!.emoji!}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{Boolean(customStatus?.text) &&
|
||||
<Text style={styles.customStatusLabel}>
|
||||
{customStatus?.text}
|
||||
</Text>
|
||||
}
|
||||
{Boolean(customStatus?.duration) &&
|
||||
<CustomStatusExpiry
|
||||
time={moment(customStatus?.expires_at)}
|
||||
theme={theme}
|
||||
textStyles={styles.customStatusExpiry}
|
||||
withinBrackets={false}
|
||||
showPrefix={true}
|
||||
showToday={true}
|
||||
showTimeCompulsory={false}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
{Boolean(header) &&
|
||||
<View style={styles.item}>
|
||||
<FormattedText
|
||||
id='channel_info.header'
|
||||
defaultMessage='Header:'
|
||||
style={styles.extraHeading}
|
||||
/>
|
||||
<Markdown
|
||||
baseTextStyle={styles.header}
|
||||
blockStyles={blockStyles}
|
||||
disableBlockQuote={true}
|
||||
disableCodeBlock={true}
|
||||
disableGallery={true}
|
||||
disableHeading={true}
|
||||
disableTables={true}
|
||||
textStyles={textStyles}
|
||||
layoutHeight={48}
|
||||
layoutWidth={100}
|
||||
theme={theme}
|
||||
imagesMetadata={headerMetadata}
|
||||
value={header}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{Boolean(createdAt && createdBy) &&
|
||||
<View style={styles.item}>
|
||||
<FormattedText
|
||||
id='channel_intro.createdBy'
|
||||
defaultMessage='Created by {user} on {date}'
|
||||
style={styles.created}
|
||||
values={created}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{Boolean(createdAt && !createdBy) &&
|
||||
<View style={styles.item}>
|
||||
<FormattedText
|
||||
id='channel_intro.createdOn'
|
||||
defaultMessage='Created on {date}'
|
||||
style={styles.created}
|
||||
values={created}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Extra;
|
||||
59
app/screens/channel_info/extra/index.ts
Normal file
59
app/screens/channel_info/extra/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
|
||||
import {observeCurrentUser, observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
|
||||
import {displayUsername, getUserCustomStatus, getUserIdFromChannelName, isCustomStatusExpired as checkCustomStatusIsExpired} from '@utils/user';
|
||||
|
||||
import Extra from './extra';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const teammateNameDisplay = observeTeammateNameDisplay(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const channelInfo = observeChannelInfo(database, channelId);
|
||||
const createdAt = channel.pipe(switchMap((c) => of$(c?.type === General.DM_CHANNEL ? 0 : c?.createAt)));
|
||||
const header = channelInfo.pipe(switchMap((ci) => of$(ci?.header)));
|
||||
const dmUser = currentUser.pipe(
|
||||
combineLatestWith(channel),
|
||||
switchMap(([user, c]) => {
|
||||
if (c?.type === General.DM_CHANNEL && user) {
|
||||
const teammateId = getUserIdFromChannelName(user.id, c.name);
|
||||
return observeUser(database, teammateId);
|
||||
}
|
||||
|
||||
return of$(undefined);
|
||||
}),
|
||||
);
|
||||
|
||||
const createdBy = channel.pipe(
|
||||
switchMap((ch) => (ch?.creatorId ? ch.creator.observe() : of$(undefined))),
|
||||
combineLatestWith(currentUser, teammateNameDisplay),
|
||||
switchMap(([creator, me, disaplySetting]) => of$(displayUsername(creator, me?.locale, disaplySetting, false))),
|
||||
);
|
||||
|
||||
const customStatus = dmUser.pipe(
|
||||
switchMap((dm) => of$(checkCustomStatusIsExpired(dm) ? undefined : getUserCustomStatus(dm))),
|
||||
);
|
||||
|
||||
return {
|
||||
createdAt,
|
||||
createdBy,
|
||||
customStatus,
|
||||
header,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Extra));
|
||||
28
app/screens/channel_info/index.ts
Normal file
28
app/screens/channel_info/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import ChannelInfo from './channel_info';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const type = channel.pipe(switchMap((c) => of$(c?.type)));
|
||||
|
||||
return {
|
||||
type,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelInfo));
|
||||
35
app/screens/channel_info/options/edit_channel/index.tsx
Normal file
35
app/screens/channel_info/options/edit_channel/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {goToScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const EditChannel = ({channelId}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const title = formatMessage({id: 'screens.channel_edit', defaultMessage: 'Edit Channel'});
|
||||
|
||||
const goToEditChannel = preventDoubleTap(async () => {
|
||||
goToScreen(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId, isModal: false});
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToEditChannel}
|
||||
label={title}
|
||||
icon='pencil-outline'
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditChannel;
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {updateChannelNotifyProps} from '@actions/remote/channel';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
ignoring: boolean;
|
||||
}
|
||||
|
||||
const IgnoreMentions = ({channelId, ignoring}: Props) => {
|
||||
const [ignored, setIgnored] = useState(ignoring);
|
||||
const serverUrl = useServerUrl();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const toggleIgnore = preventDoubleTap(() => {
|
||||
const props: Partial<ChannelNotifyProps> = {
|
||||
ignore_channel_mentions: ignoring ? 'off' : 'on',
|
||||
};
|
||||
setIgnored(!ignored);
|
||||
updateChannelNotifyProps(serverUrl, channelId, props);
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={toggleIgnore}
|
||||
label={formatMessage({id: 'channel_info.ignore_mentions', defaultMessage: 'Ignore @channel, @here, @all'})}
|
||||
icon='at'
|
||||
type='toggle'
|
||||
selected={ignored}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IgnoreMentions;
|
||||
49
app/screens/channel_info/options/ignore_mentions/index.ts
Normal file
49
app/screens/channel_info/options/ignore_mentions/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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 {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Channel} from '@constants';
|
||||
import {observeChannelSettings} from '@queries/servers/channel';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import IgnoreMentions from './ignore_mentions';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const isChannelMentionsIgnored = (channelNotifyProps?: Partial<ChannelNotifyProps>, userNotifyProps?: UserNotifyProps | null) => {
|
||||
let ignoreChannelMentionsDefault = Channel.IGNORE_CHANNEL_MENTIONS_OFF;
|
||||
|
||||
if (userNotifyProps?.channel && userNotifyProps.channel === 'false') {
|
||||
ignoreChannelMentionsDefault = Channel.IGNORE_CHANNEL_MENTIONS_ON;
|
||||
}
|
||||
|
||||
let ignoreChannelMentions = channelNotifyProps?.ignore_channel_mentions;
|
||||
if (!ignoreChannelMentions || ignoreChannelMentions === Channel.IGNORE_CHANNEL_MENTIONS_DEFAULT) {
|
||||
ignoreChannelMentions = ignoreChannelMentionsDefault as any;
|
||||
}
|
||||
|
||||
return ignoreChannelMentions !== Channel.IGNORE_CHANNEL_MENTIONS_OFF;
|
||||
};
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const settings = observeChannelSettings(database, channelId);
|
||||
const ignoring = currentUser.pipe(
|
||||
combineLatestWith(settings),
|
||||
switchMap(([u, s]) => of$(isChannelMentionsIgnored(s?.notifyProps, u?.notifyProps))),
|
||||
);
|
||||
|
||||
return {
|
||||
ignoring,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(IgnoreMentions));
|
||||
37
app/screens/channel_info/options/index.tsx
Normal file
37
app/screens/channel_info/options/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
import EditChannel from './edit_channel';
|
||||
import IgnoreMentions from './ignore_mentions';
|
||||
import Members from './members';
|
||||
import NotificationPreference from './notification_preference';
|
||||
import PinnedMessages from './pinned_messages';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
type?: ChannelType;
|
||||
}
|
||||
|
||||
const Options = ({channelId, type}: Props) => {
|
||||
return (
|
||||
<>
|
||||
{type !== General.DM_CHANNEL &&
|
||||
<IgnoreMentions channelId={channelId}/>
|
||||
}
|
||||
<NotificationPreference channelId={channelId}/>
|
||||
<PinnedMessages channelId={channelId}/>
|
||||
{type !== General.DM_CHANNEL &&
|
||||
<Members channelId={channelId}/>
|
||||
}
|
||||
{type !== General.DM_CHANNEL && type !== General.GM_CHANNEL &&
|
||||
<EditChannel channelId={channelId}/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Options;
|
||||
30
app/screens/channel_info/options/members/index.ts
Normal file
30
app/screens/channel_info/options/members/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannelInfo} from '@queries/servers/channel';
|
||||
|
||||
import Members from './members';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const info = observeChannelInfo(database, channelId);
|
||||
const count = info.pipe(
|
||||
switchMap((i) => of$(i?.memberCount || 0)),
|
||||
);
|
||||
|
||||
return {
|
||||
count,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Members));
|
||||
37
app/screens/channel_info/options/members/members.tsx
Normal file
37
app/screens/channel_info/options/members/members.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {goToScreen} from '@app/screens/navigation';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const Members = ({channelId, count}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const title = formatMessage({id: 'channel_info.members', defaultMessage: 'Members'});
|
||||
|
||||
const goToChannelMembers = preventDoubleTap(() => {
|
||||
goToScreen(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToChannelMembers}
|
||||
label={title}
|
||||
icon='account-multiple-outline'
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
info={count.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Members;
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannelSettings} from '@queries/servers/channel';
|
||||
|
||||
import NotificationPreference from './notification_preference';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const settings = observeChannelSettings(database, channelId);
|
||||
const notifyLevel = settings.pipe(
|
||||
switchMap((s) => of$(s?.notifyProps.push)),
|
||||
);
|
||||
|
||||
return {
|
||||
notifyLevel,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(NotificationPreference));
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {t} from '@app/i18n';
|
||||
import {goToScreen} from '@app/screens/navigation';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {NotificationLevel, Screens} from '@constants';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
notifyLevel: NotificationLevel;
|
||||
}
|
||||
|
||||
const NotificationPreference = ({channelId, notifyLevel}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const title = formatMessage({id: 'channel_info.mobile_notifications', defaultMessage: 'Mobile Notifications'});
|
||||
|
||||
const goToMentions = preventDoubleTap(() => {
|
||||
goToScreen(Screens.CHANNEL_MENTION, title, {channelId});
|
||||
});
|
||||
|
||||
const notificationLevelToText = () => {
|
||||
let id = '';
|
||||
let defaultMessage = '';
|
||||
switch (notifyLevel) {
|
||||
case NotificationLevel.ALL: {
|
||||
id = t('channel_info.notification.all');
|
||||
defaultMessage = 'All';
|
||||
break;
|
||||
}
|
||||
case NotificationLevel.MENTION: {
|
||||
id = t('channel_info.notification.mention');
|
||||
defaultMessage = 'Mentions';
|
||||
break;
|
||||
}
|
||||
case NotificationLevel.NONE: {
|
||||
id = t('channel_info.notification.none');
|
||||
defaultMessage = 'Never';
|
||||
break;
|
||||
}
|
||||
default:
|
||||
id = t('channel_info.notification.default');
|
||||
defaultMessage = 'Default';
|
||||
break;
|
||||
}
|
||||
|
||||
return formatMessage({id, defaultMessage});
|
||||
};
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToMentions}
|
||||
label={title}
|
||||
icon='cellphone'
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
info={notificationLevelToText()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationPreference;
|
||||
30
app/screens/channel_info/options/pinned_messages/index.ts
Normal file
30
app/screens/channel_info/options/pinned_messages/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannelInfo} from '@queries/servers/channel';
|
||||
|
||||
import PinnedMessages from './pinned_messages';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const info = observeChannelInfo(database, channelId);
|
||||
const count = info.pipe(
|
||||
switchMap((i) => of$(i?.pinnedPostCount || 0)),
|
||||
);
|
||||
|
||||
return {
|
||||
count,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(PinnedMessages));
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {goToScreen} from '@app/screens/navigation';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const PinnedMessages = ({channelId, count}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const title = formatMessage({id: 'channel_info.pinned_messages', defaultMessage: 'Pinned Messages'});
|
||||
|
||||
const goToPinnedMessages = preventDoubleTap(() => {
|
||||
goToScreen(Screens.PINNED_MESSAGES, title, {channelId});
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToPinnedMessages}
|
||||
label={title}
|
||||
icon='pin-outline'
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
info={count.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinnedMessages;
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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 {BotTag, GuestTag} from '@app/components/tag';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
displayName?: string;
|
||||
user?: UserModel;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
displayName: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
position: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 200),
|
||||
},
|
||||
tagContainer: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
tag: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
flexShrink: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const DirectMessage = ({displayName, user}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={64}
|
||||
iconSize={64}
|
||||
showStatus={true}
|
||||
statusSize={24}
|
||||
/>
|
||||
<View style={styles.titleContainer}>
|
||||
<View style={styles.displayName}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={styles.title}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
{user?.isGuest &&
|
||||
<GuestTag
|
||||
textStyle={styles.tag}
|
||||
style={styles.tagContainer}
|
||||
/>
|
||||
}
|
||||
{user?.isBot &&
|
||||
<BotTag
|
||||
textStyle={styles.tag}
|
||||
style={styles.tagContainer}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
{Boolean(user?.position) &&
|
||||
<Text style={styles.position}>
|
||||
{user?.position}
|
||||
</Text>
|
||||
}
|
||||
{Boolean(user?.isBot && user.props?.bot_description) &&
|
||||
<Text style={styles.position}>
|
||||
{user?.props?.bot_description}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectMessage;
|
||||
42
app/screens/channel_info/title/direct_message/index.ts
Normal file
42
app/screens/channel_info/title/direct_message/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import DirectMessage from './direct_message';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const user = currentUserId.pipe(
|
||||
combineLatestWith(channel),
|
||||
switchMap(([uId, ch]) => {
|
||||
if (!ch) {
|
||||
return of$(undefined);
|
||||
}
|
||||
const otherUserId = getUserIdFromChannelName(uId, ch.name);
|
||||
return observeUser(database, otherUserId);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
user,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(DirectMessage));
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
users: UserModel[];
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
},
|
||||
profile: {
|
||||
borderColor: theme.centerChannelBg,
|
||||
borderRadius: 24,
|
||||
borderWidth: 2,
|
||||
height: 48,
|
||||
width: 48,
|
||||
},
|
||||
}));
|
||||
|
||||
const GroupAvatars = ({users}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let client: Client | undefined;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const group = users.map((u, i) => {
|
||||
const pictureUrl = client!.getProfilePictureUrl(u.id, u.lastPictureUpdate);
|
||||
return (
|
||||
<FastImage
|
||||
key={pictureUrl + i.toString()}
|
||||
style={[styles.profile, {transform: [{translateX: -(i * 12)}]}]}
|
||||
source={{uri: `${serverUrl}${pictureUrl}`}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
{group}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupAvatars;
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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 {queryUsersById} from '@queries/servers/user';
|
||||
|
||||
import GroupAvatars from './avatars';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({userIds, database}: {userIds: string[]} & WithDatabaseArgs) => ({
|
||||
users: queryUsersById(database, userIds).observeWithColumns(['last_picture_update']),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(GroupAvatars));
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import GroupAvatars from './avatars';
|
||||
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
||||
type Props = {
|
||||
currentUserId: string;
|
||||
displayName?: string;
|
||||
members: ChannelMembershipModel[];
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
avatars: {
|
||||
left: 0,
|
||||
marginBottom: 8,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const GroupMessage = ({currentUserId, displayName, members}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const userIds = useMemo(() => members.map((cm) => cm.userId).filter((id) => id !== currentUserId),
|
||||
[members.length, currentUserId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GroupAvatars
|
||||
userIds={userIds}
|
||||
/>
|
||||
<Text style={styles.title}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupMessage;
|
||||
31
app/screens/channel_info/title/group_message/index.ts
Normal file
31
app/screens/channel_info/title/group_message/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import GroupMessage from './group_message';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const channel = observeChannel(database, channelId);
|
||||
const members = channel.pipe(switchMap((c) => (c ? c.members.observe() : of$([]))));
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
members,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(GroupMessage));
|
||||
28
app/screens/channel_info/title/index.ts
Normal file
28
app/screens/channel_info/title/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
||||
import Title from './title';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const displayName = channel.pipe(switchMap((c) => of$(c?.displayName)));
|
||||
|
||||
return {
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Title));
|
||||
28
app/screens/channel_info/title/public_private/index.ts
Normal file
28
app/screens/channel_info/title/public_private/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannelInfo} from '@queries/servers/channel';
|
||||
|
||||
import PublicPrivate from './public_private';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channelInfo = observeChannelInfo(database, channelId);
|
||||
const purpose = channelInfo.pipe(switchMap((ci) => of$(ci?.purpose)));
|
||||
|
||||
return {
|
||||
purpose,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(PublicPrivate));
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
displayName?: string;
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
},
|
||||
purpose: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
marginTop: 8,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
}));
|
||||
|
||||
const PublicPrivate = ({displayName, purpose}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.title}>
|
||||
{displayName}
|
||||
</Text>
|
||||
{Boolean(purpose) &&
|
||||
<Text style={styles.purpose}>
|
||||
{purpose}
|
||||
</Text>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicPrivate;
|
||||
63
app/screens/channel_info/title/title.tsx
Normal file
63
app/screens/channel_info/title/title.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
import DirectMessage from './direct_message';
|
||||
import GroupMessage from './group_message';
|
||||
import PublicPrivate from './public_private';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
displayName?: string;
|
||||
type?: ChannelType;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
marginTop: 24,
|
||||
},
|
||||
});
|
||||
|
||||
const Title = ({channelId, displayName, type}: Props) => {
|
||||
let component;
|
||||
switch (type) {
|
||||
case General.DM_CHANNEL:
|
||||
component = (
|
||||
<DirectMessage
|
||||
channelId={channelId}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case General.GM_CHANNEL:
|
||||
component = (
|
||||
<GroupMessage
|
||||
channelId={channelId}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
component = (
|
||||
<PublicPrivate
|
||||
channelId={channelId}
|
||||
displayName={displayName}
|
||||
/>
|
||||
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{component}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Title;
|
||||
@@ -12,7 +12,7 @@ import {General} from '@constants';
|
||||
import {MIN_CHANNEL_NAME_LENGTH} from '@constants/channel';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {buildNavigationButton, dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {validateDisplayName} from '@utils/channel';
|
||||
|
||||
import ChannelInfoForm from './channel_info_form';
|
||||
@@ -48,9 +48,13 @@ interface RequestAction {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const close = (componentId: string): void => {
|
||||
const close = (componentId: string, isModal: boolean): void => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal({componentId});
|
||||
if (isModal) {
|
||||
dismissModal({componentId});
|
||||
} else {
|
||||
popTopScreen(componentId);
|
||||
}
|
||||
};
|
||||
|
||||
const isDirect = (channel?: ChannelModel): boolean => {
|
||||
@@ -181,9 +185,9 @@ const CreateOrEditChannel = ({
|
||||
}
|
||||
|
||||
dispatch({type: RequestActions.COMPLETE});
|
||||
close(componentId);
|
||||
close(componentId, isModal);
|
||||
switchToChannelById(serverUrl, createdChannel.channel!.id, createdChannel.channel!.team_id);
|
||||
}, [serverUrl, type, displayName, header, purpose, isValidDisplayName]);
|
||||
}, [serverUrl, type, displayName, header, isModal, purpose, isValidDisplayName]);
|
||||
|
||||
const onUpdateChannel = useCallback(async () => {
|
||||
if (!channel) {
|
||||
@@ -198,7 +202,7 @@ const CreateOrEditChannel = ({
|
||||
const patchChannel = {
|
||||
id: channel.id,
|
||||
type: channel.type,
|
||||
display_name: isDirect(channel) ? '' : displayName,
|
||||
display_name: isDirect(channel) ? channel.displayName : displayName,
|
||||
purpose,
|
||||
header,
|
||||
} as Channel;
|
||||
@@ -213,15 +217,15 @@ const CreateOrEditChannel = ({
|
||||
return;
|
||||
}
|
||||
dispatch({type: RequestActions.COMPLETE});
|
||||
close(componentId);
|
||||
}, [channel?.id, channel?.type, displayName, header, purpose, isValidDisplayName]);
|
||||
close(componentId, isModal);
|
||||
}, [channel?.id, channel?.type, displayName, header, isModal, purpose, isValidDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
const update = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: {buttonId: string}) => {
|
||||
switch (buttonId) {
|
||||
case CLOSE_BUTTON_ID:
|
||||
close(componentId);
|
||||
close(componentId, isModal);
|
||||
break;
|
||||
case CREATE_BUTTON_ID:
|
||||
onCreateChannel();
|
||||
@@ -236,7 +240,7 @@ const CreateOrEditChannel = ({
|
||||
return () => {
|
||||
update.remove();
|
||||
};
|
||||
}, [onCreateChannel, onUpdateChannel]);
|
||||
}, [onCreateChannel, onUpdateChannel, isModal]);
|
||||
|
||||
return (
|
||||
<ChannelInfoForm
|
||||
|
||||
@@ -11,11 +11,9 @@ import FormattedText from '@components/formatted_text';
|
||||
import {CustomStatusDuration, CST} from '@constants/custom_status';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {Moment} from 'moment-timezone';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
duration: CustomStatusDuration;
|
||||
onOpenClearAfterModal: () => void;
|
||||
theme: Theme;
|
||||
@@ -51,7 +49,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const ClearAfter = ({currentUser, duration, expiresAt, onOpenClearAfterModal, theme}: Props) => {
|
||||
const ClearAfter = ({duration, expiresAt, onOpenClearAfterModal, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -60,7 +58,6 @@ const ClearAfter = ({currentUser, duration, expiresAt, onOpenClearAfterModal, th
|
||||
return (
|
||||
<View style={style.expiryTime}>
|
||||
<CustomStatusExpiry
|
||||
currentUser={currentUser}
|
||||
textStyles={style.customStatusExpiry}
|
||||
theme={theme}
|
||||
time={expiresAt.toDate()}
|
||||
|
||||
@@ -313,7 +313,7 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const {duration, emoji, expires_at, text} = this.state;
|
||||
const {customStatusExpirySupported, currentUser, intl, recentCustomStatuses, theme} = this.props;
|
||||
const {customStatusExpirySupported, intl, recentCustomStatuses, theme} = this.props;
|
||||
const isStatusSet = Boolean(emoji || text);
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -356,7 +356,6 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
/>
|
||||
{isStatusSet && customStatusExpirySupported && (
|
||||
<ClearAfter
|
||||
currentUser={currentUser}
|
||||
duration={duration}
|
||||
expiresAt={expires_at}
|
||||
onOpenClearAfterModal={this.openClearAfterModal}
|
||||
|
||||
@@ -116,7 +116,6 @@ const ClearAfterMenuItem = ({currentUser, duration, expiryTime = '', handleItemC
|
||||
{showExpiryTime && expiryTime !== '' && (
|
||||
<View style={style.rightPosition}>
|
||||
<CustomStatusExpiry
|
||||
currentUser={currentUser}
|
||||
theme={theme}
|
||||
time={moment(expiryTime).toDate()}
|
||||
textStyles={style.customStatusExpiry}
|
||||
|
||||
@@ -12,15 +12,12 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CustomStatusText from './custom_status_text';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type CustomLabelProps = {
|
||||
customStatus: UserCustomStatus;
|
||||
isCustomStatusExpirySupported: boolean;
|
||||
isStatusSet: boolean;
|
||||
showRetryMessage: boolean;
|
||||
theme: Theme;
|
||||
currentUser: UserModel;
|
||||
onClearCustomStatus: () => void;
|
||||
};
|
||||
|
||||
@@ -46,7 +43,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const CustomLabel = ({currentUser, customStatus, isCustomStatusExpirySupported, isStatusSet, onClearCustomStatus, showRetryMessage, theme}: CustomLabelProps) => {
|
||||
const CustomLabel = ({customStatus, isCustomStatusExpirySupported, isStatusSet, onClearCustomStatus, showRetryMessage, theme}: CustomLabelProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
@@ -59,7 +56,6 @@ const CustomLabel = ({currentUser, customStatus, isCustomStatusExpirySupported,
|
||||
/>
|
||||
{Boolean(isStatusSet && isCustomStatusExpirySupported && customStatus?.duration) && (
|
||||
<CustomStatusExpiry
|
||||
currentUser={currentUser}
|
||||
time={moment(customStatus?.expires_at)}
|
||||
theme={theme}
|
||||
textStyles={style.customStatusExpiryText}
|
||||
|
||||
@@ -72,7 +72,6 @@ const CustomStatus = ({isCustomStatusExpirySupported, isTablet, currentUser}: Cu
|
||||
testID='settings.sidebar.custom_status.action'
|
||||
labelComponent={
|
||||
<CustomLabel
|
||||
currentUser={currentUser}
|
||||
theme={theme}
|
||||
customStatus={customStatus!}
|
||||
isCustomStatusExpirySupported={isCustomStatusExpirySupported}
|
||||
|
||||
@@ -73,6 +73,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
case Screens.CHANNEL_INFO:
|
||||
screen = withServerDatabase(require('@screens/channel_info').default);
|
||||
break;
|
||||
case Screens.CODE:
|
||||
screen = withServerDatabase(require('@screens/code').default);
|
||||
break;
|
||||
|
||||
@@ -19,6 +19,8 @@ class EphemeralStore {
|
||||
private addingTeam = new Set<string>();
|
||||
private joiningChannels = new Set<string>();
|
||||
private leavingChannels = new Set<string>();
|
||||
private archivingChannels = new Set<string>();
|
||||
private convertingChannels = new Set<string>();
|
||||
|
||||
addNavigationComponentId = (componentId: string) => {
|
||||
this.addToNavigationComponentIdStack(componentId);
|
||||
@@ -134,6 +136,32 @@ class EphemeralStore {
|
||||
}
|
||||
};
|
||||
|
||||
// Ephemeral control when (un)archiving a channel locally
|
||||
addArchivingChannel = (channelId: string) => {
|
||||
this.archivingChannels.add(channelId);
|
||||
};
|
||||
|
||||
isArchivingChannel = (channelId: string) => {
|
||||
return this.archivingChannels.has(channelId);
|
||||
};
|
||||
|
||||
removeArchivingChannel = (channelId: string) => {
|
||||
this.archivingChannels.delete(channelId);
|
||||
};
|
||||
|
||||
// Ephemeral control when converting a channel to private locally
|
||||
addConvertingChannel = (channelId: string) => {
|
||||
this.convertingChannels.add(channelId);
|
||||
};
|
||||
|
||||
isConvertingChannel = (channelId: string) => {
|
||||
return this.convertingChannels.has(channelId);
|
||||
};
|
||||
|
||||
removeConvertingChannel = (channelId: string) => {
|
||||
this.convertingChannels.delete(channelId);
|
||||
};
|
||||
|
||||
// Ephemeral control when leaving a channel locally
|
||||
addLeavingChannel = (channelId: string) => {
|
||||
this.leavingChannels.add(channelId);
|
||||
|
||||
@@ -242,6 +242,7 @@ export const getMarkdownImageSize = (
|
||||
sourceSize?: SourceSize,
|
||||
knownSize?: PostImage,
|
||||
layoutWidth?: number,
|
||||
layoutHeight?: number,
|
||||
) => {
|
||||
let ratioW;
|
||||
let ratioH;
|
||||
@@ -277,5 +278,5 @@ export const getMarkdownImageSize = (
|
||||
|
||||
// When no metadata and source size is not specified (full size svg's)
|
||||
const width = layoutWidth || getViewPortWidth(isReplyPost, isTablet);
|
||||
return {width, height: width};
|
||||
return {width, height: layoutHeight || width};
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import {toTitleCase} from '@utils/helpers';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export function displayUsername(user?: UserProfile | UserModel, locale?: string, teammateDisplayNameSetting?: string, useFallbackUsername = true) {
|
||||
export function displayUsername(user?: UserProfile | UserModel | null, locale?: string, teammateDisplayNameSetting?: string, useFallbackUsername = true) {
|
||||
let name = useFallbackUsername ? getLocalizedMessage(locale || DEFAULT_LOCALE, t('channel_loader.someone'), 'Someone') : '';
|
||||
|
||||
if (user) {
|
||||
|
||||
@@ -99,22 +99,53 @@
|
||||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_header.info": "View info",
|
||||
"channel_header.member_count": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_info.alert_retry": "Try Again",
|
||||
"channel_info.alertNo": "No",
|
||||
"channel_info.alertYes": "Yes",
|
||||
"channel_info.archive": "Archive Channel",
|
||||
"channel_info.archive_description": "Are you sure you want to archive the {term} {name}?",
|
||||
"channel_info.archive_failed": "An error occurred trying to archive the channel {displayName}",
|
||||
"channel_info.archive_title": "Archive {term}",
|
||||
"channel_info.close": "Close",
|
||||
"channel_info.close_dm": "Close direct message",
|
||||
"channel_info.close_dm_channel": "Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.",
|
||||
"channel_info.close_gm": "Close group message",
|
||||
"channel_info.close_gm_channel": "Are you sure you want to close this group message? This will remove it from your home screen, but you can always open it again.",
|
||||
"channel_info.convert_failed": "We were unable to convert {displayName} to a private channel.",
|
||||
"channel_info.convert_private": "Convert to private channel",
|
||||
"channel_info.convert_private_description": "When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?",
|
||||
"channel_info.convert_private_success": "{displayName} is now a private channel.",
|
||||
"channel_info.convert_private_title": "Convert {displayName} to a private channel?",
|
||||
"channel_info.copied": "Copied",
|
||||
"channel_info.copy_link": "Copy Link",
|
||||
"channel_info.custom_status": "Custom status:",
|
||||
"channel_info.edit_header": "Edit Header",
|
||||
"channel_info.error_close": "Close",
|
||||
"channel_info.favorite": "Favorite",
|
||||
"channel_info.favorited": "Favorited",
|
||||
"channel_info.header": "Header:",
|
||||
"channel_info.ignore_mentions": "Ignore @channel, @here, @all",
|
||||
"channel_info.leave": "Leave",
|
||||
"channel_info.leave_channel": "Leave channel",
|
||||
"channel_info.leave_private_channel": "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.",
|
||||
"channel_info.leave_public_channel": "Are you sure you want to leave the public channel {displayName}? You can always rejoin.",
|
||||
"channel_info.members": "Members",
|
||||
"channel_info.mobile_notifications": "Mobile Notifications",
|
||||
"channel_info.muted": "Mute",
|
||||
"channel_info.notification.all": "All",
|
||||
"channel_info.notification.default": "Default",
|
||||
"channel_info.notification.mention": "Mentions",
|
||||
"channel_info.notification.none": "Never",
|
||||
"channel_info.pinned_messages": "Pinned Messages",
|
||||
"channel_info.private_channel": "Private Channel",
|
||||
"channel_info.public_channel": "Public Channel",
|
||||
"channel_info.set_header": "Set Header",
|
||||
"channel_info.unarchive": "Unarchive Channel",
|
||||
"channel_info.unarchive_description": "Are you sure you want to unarchive the {term} {name}?",
|
||||
"channel_info.unarchive_failed": "An error occurred trying to unarchive the channel {displayName}",
|
||||
"channel_info.unarchive_title": "Unarchive {term}",
|
||||
"channel_intro.createdBy": "Created by {user} on {date}",
|
||||
"channel_intro.createdOn": "Created on {date}",
|
||||
"channel_list.channels_category": "Channels",
|
||||
"channel_list.dms_category": "Direct messages",
|
||||
"channel_list.favorites_category": "Favorites",
|
||||
@@ -579,8 +610,11 @@
|
||||
"screen.search.header.messages": "Messages",
|
||||
"screen.search.placeholder": "Search messages & files",
|
||||
"screen.search.title": "Search",
|
||||
"screens.channel_edit": "Edit Channel",
|
||||
"screens.channel_edit_header": "Edit Channel Header",
|
||||
"screens.channel_info": "Channel Info",
|
||||
"screens.channel_info.dm": "Direct message info",
|
||||
"screens.channel_info.gm": "Group message info",
|
||||
"search_bar.search": "Search",
|
||||
"select_team.description": "You are not yet a member of any teams. Select one below to get started.",
|
||||
"select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.",
|
||||
|
||||
8
types/api/channels.d.ts
vendored
8
types/api/channels.d.ts
vendored
@@ -8,11 +8,13 @@ type ChannelStats = {
|
||||
pinnedpost_count: number;
|
||||
};
|
||||
|
||||
type NotificationLevel = 'default' | 'all' | 'mention' | 'none';
|
||||
|
||||
type ChannelNotifyProps = {
|
||||
desktop: 'default' | 'all' | 'mention' | 'none';
|
||||
email: 'default' | 'all' | 'mention' | 'none';
|
||||
desktop: NotificationLevel;
|
||||
email: NotificationLevel;
|
||||
mark_unread: 'all' | 'mention';
|
||||
push: 'default' | 'all' | 'mention' | 'none';
|
||||
push: NotificationLevel;
|
||||
ignore_channel_mentions: 'default' | 'off' | 'on';
|
||||
};
|
||||
type Channel = {
|
||||
|
||||
2
types/api/users.d.ts
vendored
2
types/api/users.d.ts
vendored
@@ -43,6 +43,8 @@ type UserProfile = {
|
||||
last_picture_update: number;
|
||||
remote_id?: string;
|
||||
status?: string;
|
||||
bot_description?: string;
|
||||
bot_last_icon_update?: number;
|
||||
};
|
||||
|
||||
type UsersState = {
|
||||
|
||||
Reference in New Issue
Block a user