forked from Ivasoft/mattermost-mobile
[Gekidou MM-39733] Port Create Channel from V1 (#6067)
* copy directly from v1. will get working and then convert class to functional components
* screen showing up correctly. Need to convert lifestyle methods
* create channel button working
* save before refactor in prep for bringing in edit_channel functionality
* change function naming
* clean up lint
* clean up for PR review
* clean up for PR review
* remove hoardcoded styles
* add edit_channel screen
* add handlePatchChannel
* add custom useFormInput hook. use edit screen for both create and edit screens. edit or create screen mode determined by channel prop passed in as a channel or null
* rename edit_channel to create_or_edit_channel
* displayname, header, and purpose are now an object with value and onChange props, created from the useFormInput hook. Now only need to pass this new FormInput Prop to the edit_channel_info component and deconstruct there to get the onChange and value
* fix some lint errors
* fix some lint errors
* remove empty line
* pass intl into utils validate functions because they are not Hooks. add validation for displayName including translations.
* Move useFormInput hook to its own hooks file and import
* simplify
* remove editing prop. Was used to determine if the right button was enabled. It was always true for edit_channel screen and always false for create channel screen. The enableRightButton prop call back is was also used for the same reason.
* remove channeUrl editing references. This was not implemented on v1
* pass editing prop back into component and add back logic. When editing one field must change. when creating, just need to check that name is provided
* lint fixes
* fix typing issue for channel types
* scrolling ref should be fixed. Linting should pass now
* Linting should pass now
* require id field in partial Channel. fixes tsc
* remove everything related to renaming the channel URL. This has never been requred for mobile
* manage state with useReducer so that all actions/state in one location. This also removes the number of onXXX functions and reduces the number of functions in the component
* reorganize code. useEffects are at top. Move type and interfaces outside of function component
* Fix lint
* nit: invert if statement checking a negative
* use cneterChannelColor. in figma this is center channel text, but I verified theme color by comparing to SSO login text color
* Simple snapshot tests as a start
* Add more tests
* update snapshot
* add snapshot tests. Add tests for button enabling and disabling
* simplify test with destructuring.
* PR feedback. formatting changes. get user and teamid from one call
* remove FormInput hook and use value/setvalue convention for controlled components
* no need to setChannelDisplayName after creating/updating channel
* Just pass the setXXX function. Don't need to create as separate callback
* modify floatingTextInput component to allow placeholder text
* remove InteractionManager. PR nits
* mv EditChannelComponent into create_or_edit screen. Rename component from EditChannelInfo to ChannelInfoForm
* correct import path
* add IntlShape Type to function input. Wrap screen with withServerDatabase, not withIntl
* remove state setting function calls from inside the reducer. move close function outside of the component. remove setRightButton and rightButton and place rightbutton in initial appState
* move editing const after useX oneliners and before useCallback, useEffect, and useReducers
* rightButton
- useMemo to memoize an object with dependencies
- move out of the appState
emitCanSaveChannel
- wrap with useCallback
onCreateChannel
- wrap with useCallback
onUpdateChannel
- wrap with useCallback
useEffect Navigation
- use the callbacks as dependencies in stead of the depencies of those
callbacks.
* wrap all formatted message with useMemo()
wrap all onXXXChangeText with useCallback and add deps
move all oneliner derived constants directly after useState useMemo
* remove useMemo from formatted text
* switchToCHannel is still not working. failing at
const channel: ChannelModel = await member.channel.fetch();
* use prepareMyChannelsForTeam to update db tables for new channel
* add placeholder text color
* Attach open edit channel screen to `Set Header` button in channel intro view
port SectionItem from V1 and us to add a Switch for setting private/public channel
hook up the plus icon in the channel list header to create a channel (temporary fix to allow debugging)
add new queryChannelsInfoById and queryCurrentChannelInfo query functions
update text for create screen text inputs
* Fix styles and fix actions
* Add autocomplete, fix patch, and address design feedback
* Address feedback
* Add margin between icon and label on Make Private
* Address feedback
* Address feedback
* Address feedback and fix channel list not updating when the channel gets created
* Address feedback and directly add the channel to the default category
* Render at-mentions as Members if no channelId is set
* Display autocomplete on iOS
Co-authored-by: Jason Frerich <jason.frerich@mattermost.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
committed by
GitHub
parent
328f029a93
commit
e2e54b3bca
@@ -39,12 +39,12 @@ export const storeCategories = async (serverUrl: string, categories: CategoryWit
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const preparedCategories = prepareCategories(operator, categories);
|
||||
if (preparedCategories) {
|
||||
modelPromises.push(...preparedCategories);
|
||||
modelPromises.push(preparedCategories);
|
||||
}
|
||||
|
||||
const preparedCategoryChannels = prepareCategoryChannels(operator, categories);
|
||||
if (preparedCategoryChannels) {
|
||||
modelPromises.push(...preparedCategoryChannels);
|
||||
modelPromises.push(preparedCategoryChannels);
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
|
||||
@@ -12,12 +12,12 @@ import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel} from '@queries/servers/channel';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {getDirectChannelName} from '@utils/channel';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName} from '@utils/channel';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
@@ -107,6 +107,104 @@ export const fetchChannelByName = async (serverUrl: string, teamId: string, chan
|
||||
}
|
||||
};
|
||||
|
||||
export const createChannel = async (serverUrl: string, displayName: string, purpose: string, header: string, type: ChannelType) => {
|
||||
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 {currentUserId, currentTeamId} = await getCommonSystemValues(database);
|
||||
const name = generateChannelNameFromDisplayName(displayName);
|
||||
const channel = {
|
||||
creator_id: currentUserId,
|
||||
team_id: currentTeamId,
|
||||
display_name: displayName,
|
||||
header,
|
||||
name,
|
||||
purpose,
|
||||
type,
|
||||
} as Channel;
|
||||
|
||||
const channelData = await client.createChannel(channel);
|
||||
|
||||
const member = await client.getChannelMember(channelData.id, currentUserId);
|
||||
|
||||
const models: Model[] = [];
|
||||
const channelModels = await prepareMyChannelsForTeam(operator, channelData.team_id, [channelData], [member]);
|
||||
if (channelModels?.length) {
|
||||
const resolvedModels = await Promise.all(channelModels);
|
||||
models.push(...resolvedModels.flat());
|
||||
}
|
||||
const categoriesModels = await operator.handleCategoryChannels({categoryChannels: [{
|
||||
category_id: `channels_${currentUserId}_${currentTeamId}`,
|
||||
channel_id: channelData.id,
|
||||
sort_order: 0,
|
||||
id: `${currentTeamId}_${channelData.id}`,
|
||||
}],
|
||||
prepareRecordsOnly: true});
|
||||
if (categoriesModels?.length) {
|
||||
models.push(...categoriesModels);
|
||||
}
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
fetchChannelStats(serverUrl, channelData.id, false);
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const patchChannel = async (serverUrl: string, channelPatch: Partial<Channel> & {id: 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 channelData = await client.patchChannel(channelPatch.id, channelPatch);
|
||||
const models = [];
|
||||
const channelInfo = (await getChannelInfo(operator.database, channelData.id));
|
||||
if (channelInfo && (channelInfo.purpose !== channelData.purpose || channelInfo.header !== channelData.header)) {
|
||||
channelInfo.prepareUpdate((v) => {
|
||||
v.purpose = channelData.purpose;
|
||||
v.header = channelData.header;
|
||||
});
|
||||
models.push(channelInfo);
|
||||
}
|
||||
const channel = await getChannelById(operator.database, channelData.id);
|
||||
if (channel && (channel.displayName !== channelData.display_name || channel.type !== channelData.type)) {
|
||||
channel.prepareUpdate((v) => {
|
||||
v.displayName = channelData.display_name;
|
||||
v.type = channelData.type;
|
||||
});
|
||||
models.push(channel);
|
||||
}
|
||||
if (models?.length) {
|
||||
await operator.batchRecords(models.flat());
|
||||
}
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchChannelCreator = async (serverUrl: string, channelId: string, fetchOnly = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
||||
@@ -323,12 +323,17 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
autocompleteUsers = async (name: string, teamId: string, channelId?: string, options = {
|
||||
limit: General.AUTOCOMPLETE_LIMIT_DEFAULT,
|
||||
}) => {
|
||||
return this.doFetch(`${this.getUsersRoute()}/autocomplete${buildQueryString({
|
||||
const query: Dictionary<any> = {
|
||||
in_team: teamId,
|
||||
in_channel: channelId,
|
||||
name,
|
||||
limit: options.limit,
|
||||
})}`, {
|
||||
};
|
||||
if (channelId) {
|
||||
query.in_channel = channelId;
|
||||
}
|
||||
if (options.limit) {
|
||||
query.limit = options.limit;
|
||||
}
|
||||
return this.doFetch(`${this.getUsersRoute()}/autocomplete${buildQueryString(query)}`, {
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -187,6 +187,7 @@ const AtMention = ({
|
||||
setUsersInChannel(receivedUsers.users.length ? receivedUsers.users : emptyProfileList);
|
||||
setUsersOutOfChannel(receivedUsers.out_of_channel?.length ? receivedUsers.out_of_channel : emptyProfileList);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, 200), []);
|
||||
|
||||
@@ -314,7 +315,8 @@ const AtMention = ({
|
||||
|
||||
useEffect(() => {
|
||||
const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm);
|
||||
const newSections = makeSections(teamMembers, usersInChannel, usersOutOfChannel, groups, showSpecialMentions, isSearch);
|
||||
const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0);
|
||||
const newSections = makeSections(teamMembers, usersInChannel, usersOutOfChannel, groups, showSpecialMentions, buildMemberSection);
|
||||
const nSections = newSections.length;
|
||||
|
||||
if (!loading && !nSections && noResultsTerm == null) {
|
||||
@@ -322,7 +324,7 @@ const AtMention = ({
|
||||
}
|
||||
setSections(nSections ? newSections : empytSectionList);
|
||||
onShowingChange(Boolean(nSections));
|
||||
}, [usersInChannel, usersOutOfChannel, teamMembers, groups, loading]);
|
||||
}, [usersInChannel, usersOutOfChannel, teamMembers, groups, loading, channelId]);
|
||||
|
||||
if (sections.length === 0 || noResultsTerm != null) {
|
||||
// If we are not in an active state or the mention has been completed return null so nothing is rendered
|
||||
|
||||
@@ -57,8 +57,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
type Props = {
|
||||
cursorPosition: number;
|
||||
postInputTop: number;
|
||||
rootId: string;
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
channelId?: string;
|
||||
fixedBottomPosition?: boolean;
|
||||
isSearch?: boolean;
|
||||
value: string;
|
||||
@@ -66,8 +66,9 @@ type Props = {
|
||||
isAppsEnabled: boolean;
|
||||
nestedScrollEnabled?: boolean;
|
||||
updateValue: (v: string) => void;
|
||||
hasFilesAttached: boolean;
|
||||
hasFilesAttached?: boolean;
|
||||
maxHeightOverride?: number;
|
||||
inPost?: boolean;
|
||||
}
|
||||
|
||||
const Autocomplete = ({
|
||||
@@ -85,6 +86,7 @@ const Autocomplete = ({
|
||||
nestedScrollEnabled = false,
|
||||
updateValue,
|
||||
hasFilesAttached,
|
||||
inPost = false,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
@@ -154,7 +156,7 @@ const Autocomplete = ({
|
||||
testID='autocomplete'
|
||||
style={containerStyles}
|
||||
>
|
||||
{isAppsEnabled && (
|
||||
{isAppsEnabled && channelId && (
|
||||
<AppSlashSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
@@ -195,9 +197,10 @@ const Autocomplete = ({
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
rootId={rootId}
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
inPost={inPost}
|
||||
/>
|
||||
}
|
||||
{showCommands &&
|
||||
{showCommands && channelId &&
|
||||
<SlashSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
|
||||
@@ -70,11 +70,12 @@ type Props = {
|
||||
maxListHeight: number;
|
||||
updateValue: (v: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
rootId: string;
|
||||
rootId?: string;
|
||||
value: string;
|
||||
nestedScrollEnabled: boolean;
|
||||
skinTone: string;
|
||||
hasFilesAttached: boolean;
|
||||
hasFilesAttached?: boolean;
|
||||
inPost?: boolean;
|
||||
}
|
||||
const EmojiSuggestion = ({
|
||||
cursorPosition,
|
||||
@@ -86,7 +87,8 @@ const EmojiSuggestion = ({
|
||||
value,
|
||||
nestedScrollEnabled,
|
||||
skinTone,
|
||||
hasFilesAttached,
|
||||
hasFilesAttached = false,
|
||||
inPost = true,
|
||||
}: Props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
@@ -121,7 +123,7 @@ const EmojiSuggestion = ({
|
||||
const showingElements = Boolean(data.length);
|
||||
|
||||
const completeSuggestion = useCallback((emoji: string) => {
|
||||
if (!hasFilesAttached) {
|
||||
if (!hasFilesAttached && inPost) {
|
||||
const match = value.match(REACTION_REGEX);
|
||||
if (match) {
|
||||
handleReactionToLatestPost(serverUrl, emoji, match[1] === '+', rootId);
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import {fetchSuggestions} from '@actions/remote/command';
|
||||
import IntegrationsManager from '@app/init/integrations_manager';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@init/analytics';
|
||||
import IntegrationsManager from '@init/integrations_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {AppCommandParser} from './app_command_parser/app_command_parser';
|
||||
|
||||
@@ -8,7 +8,7 @@ import FastImage from 'react-native-fast-image';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {SvgXml} from 'react-native-svg';
|
||||
|
||||
import CompassIcon from '@app/components/compass_icon';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {COMMAND_SUGGESTION_ERROR} from '@constants/apps';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
@@ -32,8 +32,11 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
|
||||
}, [intl, theme]);
|
||||
|
||||
const createNewChannel = useCallback(async () => {
|
||||
// To be added
|
||||
}, [intl, theme]);
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'mobile.create_channel.title', defaultMessage: 'New channel'});
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title);
|
||||
}, [intl]);
|
||||
|
||||
const openDirectMessage = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// Note: This file has been adapted from the library https://github.com/csath/react-native-reanimated-text-input
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useState, useEffect, useRef, useImperativeHandle, forwardRef} from 'react';
|
||||
import {GestureResponderEvent, NativeSyntheticEvent, Platform, TargetedEvent, Text, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
|
||||
import React, {useState, useEffect, useRef, useImperativeHandle, forwardRef, useMemo, useCallback} from 'react';
|
||||
import {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, Platform, StyleProp, TargetedEvent, Text, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
|
||||
import Animated, {useCode, interpolateNode, EasingNode, Value, set, Clock} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -17,6 +17,28 @@ const DEFAULT_INPUT_HEIGHT = 48;
|
||||
const BORDER_DEFAULT_WIDTH = 1;
|
||||
const BORDER_FOCUSED_WIDTH = 2;
|
||||
|
||||
const getTopStyle = (styles: any, animation: Value<0|1>, inputText?: string) => {
|
||||
if (inputText) {
|
||||
return getLabelPositions(styles.textInput, styles.label, styles.smallLabel)[1];
|
||||
}
|
||||
|
||||
return interpolateNode(animation, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [...getLabelPositions(styles.textInput, styles.label, styles.smallLabel)],
|
||||
});
|
||||
};
|
||||
|
||||
const getFontSize = (styles: any, animation: Value<0|1>, inputText?: string) => {
|
||||
if (inputText) {
|
||||
return styles.smallLabel.fontSize;
|
||||
}
|
||||
|
||||
return interpolateNode(animation, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [styles.textInput.fontSize, styles.smallLabel.fontSize],
|
||||
});
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
height: DEFAULT_INPUT_HEIGHT + (2 * BORDER_DEFAULT_WIDTH),
|
||||
@@ -101,12 +123,15 @@ type FloatingTextInputProps = TextInputProps & {
|
||||
errorIcon?: string;
|
||||
isKeyboardInput?: boolean;
|
||||
label: string;
|
||||
multiline?: boolean;
|
||||
onBlur?: (event: NativeSyntheticEvent<TargetedEvent>) => void;
|
||||
onFocus?: (e: NativeSyntheticEvent<TargetedEvent>) => void;
|
||||
onPress?: (e: GestureResponderEvent) => void;
|
||||
onLayout?: (e: LayoutChangeEvent) => void;
|
||||
placeholder?: string;
|
||||
showErrorIcon?: boolean;
|
||||
theme: Theme;
|
||||
testID?: string;
|
||||
theme: Theme;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -120,7 +145,10 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
onPress = undefined,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onLayout,
|
||||
showErrorIcon = true,
|
||||
placeholder,
|
||||
multiline,
|
||||
theme,
|
||||
value = '',
|
||||
textInputStyle,
|
||||
@@ -171,86 +199,106 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
[value],
|
||||
);
|
||||
|
||||
const focusStyle = {
|
||||
top: interpolateNode(animation, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [...getLabelPositions(styles.textInput, styles.label, styles.smallLabel)],
|
||||
}),
|
||||
fontSize: interpolateNode(animation, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [styles.textInput.fontSize, styles.smallLabel.fontSize],
|
||||
}),
|
||||
backgroundColor: (
|
||||
focusedLabel ? theme.centerChannelBg : 'transparent'
|
||||
),
|
||||
paddingHorizontal: focusedLabel ? 4 : 0,
|
||||
color: styles.label.color,
|
||||
};
|
||||
|
||||
const onTextInputBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => onExecution(e,
|
||||
const onTextInputBlur = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => onExecution(e,
|
||||
() => {
|
||||
setIsFocusLabel(Boolean(value));
|
||||
setIsFocused(false);
|
||||
},
|
||||
onBlur,
|
||||
);
|
||||
), [onBlur]);
|
||||
|
||||
const onTextInputFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => onExecution(e,
|
||||
const onTextInputFocus = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => onExecution(e,
|
||||
() => {
|
||||
setIsFocusLabel(true);
|
||||
setIsFocused(true);
|
||||
},
|
||||
onFocus,
|
||||
);
|
||||
), [onFocus]);
|
||||
|
||||
const onAnimatedTextPress = () => {
|
||||
const onAnimatedTextPress = useCallback(() => {
|
||||
return focused ? null : inputRef?.current?.focus();
|
||||
};
|
||||
}, [focused]);
|
||||
|
||||
const shouldShowError = (!focused && error);
|
||||
const onPressAction = !isKeyboardInput && editable && onPress ? onPress : undefined;
|
||||
|
||||
let textInputColorStyles;
|
||||
let labelColorStyles;
|
||||
const combinedContainerStyle = useMemo(() => {
|
||||
const res = [styles.container];
|
||||
if (multiline) {
|
||||
res.push({height: 100 + (2 * BORDER_DEFAULT_WIDTH)});
|
||||
}
|
||||
res.push(containerStyle);
|
||||
return res;
|
||||
}, [styles, containerStyle, multiline]);
|
||||
|
||||
if (focused) {
|
||||
textInputColorStyles = {borderColor: theme.buttonBg};
|
||||
labelColorStyles = {color: theme.buttonBg};
|
||||
} else if (shouldShowError) {
|
||||
textInputColorStyles = {borderColor: theme.errorTextColor};
|
||||
}
|
||||
const combinedTextInputStyle = useMemo(() => {
|
||||
const res: StyleProp<TextStyle> = [styles.textInput];
|
||||
res.push({
|
||||
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
|
||||
height: DEFAULT_INPUT_HEIGHT + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
|
||||
});
|
||||
|
||||
const textInputBorder = {
|
||||
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
|
||||
height: DEFAULT_INPUT_HEIGHT + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
|
||||
};
|
||||
const combinedTextInputStyle = [styles.textInput, textInputBorder, textInputColorStyles, textInputStyle];
|
||||
const textAnimatedTextStyle = [styles.label, focusStyle, labelColorStyles, labelTextStyle];
|
||||
if (focused) {
|
||||
res.push({borderColor: theme.buttonBg});
|
||||
} else if (shouldShowError) {
|
||||
res.push({borderColor: theme.errorTextColor});
|
||||
}
|
||||
|
||||
if (error && !focused) {
|
||||
textAnimatedTextStyle.push({color: theme.errorTextColor});
|
||||
}
|
||||
res.push(textInputStyle);
|
||||
|
||||
if (multiline) {
|
||||
res.push({height: 100, textAlignVertical: 'top'});
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline]);
|
||||
|
||||
const textAnimatedTextStyle = useMemo(() => {
|
||||
const res = [styles.label];
|
||||
const inputText = value || placeholder;
|
||||
res.push({
|
||||
top: getTopStyle(styles, animation, inputText),
|
||||
fontSize: getFontSize(styles, animation, inputText),
|
||||
backgroundColor: (
|
||||
focusedLabel || inputText ? theme.centerChannelBg : 'transparent'
|
||||
),
|
||||
paddingHorizontal: focusedLabel || inputText ? 4 : 0,
|
||||
color: styles.label.color,
|
||||
});
|
||||
|
||||
if (focused) {
|
||||
res.push({color: theme.buttonBg});
|
||||
}
|
||||
|
||||
res.push(labelTextStyle);
|
||||
|
||||
if (shouldShowError) {
|
||||
res.push({color: theme.errorTextColor});
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [theme, styles, focused, shouldShowError, labelTextStyle, placeholder, animation, focusedLabel]);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPressAction}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
{
|
||||
<Animated.Text
|
||||
onPress={onAnimatedTextPress}
|
||||
style={textAnimatedTextStyle}
|
||||
suppressHighlighting={true}
|
||||
>
|
||||
{label}
|
||||
</Animated.Text>
|
||||
}
|
||||
<View style={combinedContainerStyle}>
|
||||
<Animated.Text
|
||||
onPress={onAnimatedTextPress}
|
||||
style={textAnimatedTextStyle}
|
||||
suppressHighlighting={true}
|
||||
>
|
||||
{label}
|
||||
</Animated.Text>
|
||||
<TextInput
|
||||
{...props}
|
||||
editable={isKeyboardInput && editable}
|
||||
style={combinedTextInputStyle}
|
||||
placeholder=''
|
||||
placeholderTextColor='transparent'
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={styles.label.color}
|
||||
multiline={multiline}
|
||||
value={value}
|
||||
pointerEvents={isKeyboardInput ? 'auto' : 'none'}
|
||||
onFocus={onTextInputFocus}
|
||||
|
||||
161
app/components/section_item/index.tsx
Normal file
161
app/components/section_item/index.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} 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';
|
||||
|
||||
const ActionTypes = {
|
||||
ARROW: 'arrow',
|
||||
DEFAULT: 'default',
|
||||
TOGGLE: 'toggle',
|
||||
SELECT: 'select',
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
singleContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 45,
|
||||
},
|
||||
doubleContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
height: 69,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
label: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 600, 'SemiBold'),
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
...typography('Body', 400, 'Regular'),
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
marginTop: 3,
|
||||
},
|
||||
arrow: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.25),
|
||||
fontSize: 24,
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
action: (value: string | boolean) => void;
|
||||
actionType: string;
|
||||
actionValue?: string;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
description: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const SectionItem = ({testID = 'sectionItem', action, actionType, actionValue, label, selected, description, icon}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let actionComponent;
|
||||
if (actionType === ActionTypes.SELECT && selected) {
|
||||
const selectStyle = [style.arrow, {color: theme.linkColor}];
|
||||
actionComponent = (
|
||||
<CompassIcon
|
||||
name='check'
|
||||
style={selectStyle}
|
||||
testID={`${testID}.selected`}
|
||||
/>
|
||||
);
|
||||
} else if (actionType === ActionTypes.TOGGLE) {
|
||||
actionComponent = (
|
||||
<Switch
|
||||
onValueChange={action}
|
||||
value={selected}
|
||||
/>
|
||||
);
|
||||
} else if (actionType === ActionTypes.ARROW) {
|
||||
actionComponent = (
|
||||
<CompassIcon
|
||||
name='chevron-right'
|
||||
style={style.arrow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
action(actionValue || '');
|
||||
}, [actionValue, action]);
|
||||
|
||||
const labelStyle = useMemo(() => {
|
||||
if (icon) {
|
||||
return [style.label, {marginLeft: 4}];
|
||||
}
|
||||
return style.label;
|
||||
}, [Boolean(icon), style]);
|
||||
|
||||
const component = (
|
||||
<View
|
||||
testID={testID}
|
||||
style={style.container}
|
||||
>
|
||||
<View style={description ? style.doubleContainer : style.singleContainer}>
|
||||
<View style={style.labelContainer}>
|
||||
{Boolean(icon) && (
|
||||
<CompassIcon
|
||||
name={icon!}
|
||||
size={24}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.6)}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={labelStyle}
|
||||
testID={`${testID}.label`}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={style.description}
|
||||
testID={`${testID}.description`}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
{actionComponent}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT || actionType === ActionTypes.ARROW) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
{component}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
export default SectionItem;
|
||||
10
app/constants/channel.ts
Normal file
10
app/constants/channel.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const MIN_CHANNEL_NAME_LENGTH = 1;
|
||||
export const MAX_CHANNEL_NAME_LENGTH = 64;
|
||||
|
||||
export default {
|
||||
MAX_CHANNEL_NAME_LENGTH,
|
||||
MIN_CHANNEL_NAME_LENGTH,
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import ActionType from './action_type';
|
||||
import Apps from './apps';
|
||||
import Channel from './channel';
|
||||
import {CustomStatusDuration} from './custom_status';
|
||||
import Database from './database';
|
||||
import DeepLink from './deep_linking';
|
||||
@@ -30,6 +31,7 @@ export {
|
||||
ActionType,
|
||||
Apps,
|
||||
CustomStatusDuration,
|
||||
Channel,
|
||||
Database,
|
||||
DeepLink,
|
||||
Device,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const APP_FORM = 'AppForm';
|
||||
export const BOTTOM_SHEET = 'BottomSheet';
|
||||
export const BROWSE_CHANNELS = 'BrowseChannels';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
|
||||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_DETAILS = 'ChannelDetails';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
@@ -44,6 +45,7 @@ export default {
|
||||
BOTTOM_SHEET,
|
||||
BROWSE_CHANNELS,
|
||||
CHANNEL,
|
||||
CREATE_OR_EDIT_CHANNEL,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_EDIT,
|
||||
CHANNEL_DETAILS,
|
||||
|
||||
@@ -28,12 +28,7 @@ export const queryCategoriesByTeamIds = (database: Database, teamIds: string[])
|
||||
};
|
||||
|
||||
export const prepareCategories = (operator: ServerDataOperator, categories: CategoryWithChannels[]) => {
|
||||
try {
|
||||
const categoryRecords = operator.handleCategories({categories, prepareRecordsOnly: true});
|
||||
return [categoryRecords];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return operator.handleCategories({categories, prepareRecordsOnly: true});
|
||||
};
|
||||
|
||||
export const prepareCategoryChannels = (
|
||||
@@ -55,11 +50,10 @@ export const prepareCategoryChannels = (
|
||||
});
|
||||
|
||||
if (categoryChannels.length) {
|
||||
const categoryChannelRecords = operator.handleCategoryChannels({categoryChannels, prepareRecordsOnly: true});
|
||||
return [categoryChannelRecords];
|
||||
return operator.handleCategoryChannels({categoryChannels, prepareRecordsOnly: true});
|
||||
}
|
||||
|
||||
return [];
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -184,6 +184,15 @@ export const queryChannelsById = (database: Database, channelIds: string[]) => {
|
||||
return database.get<ChannelModel>(CHANNEL).query(Q.where('id', Q.oneOf(channelIds)));
|
||||
};
|
||||
|
||||
export const getChannelInfo = async (database: Database, channelId: string) => {
|
||||
try {
|
||||
const info = await database.get<ChannelInfoModel>(CHANNEL_INFO).find(channelId);
|
||||
return info;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDefaultChannelForTeam = async (database: Database, teamId: string) => {
|
||||
let channel: ChannelModel|undefined;
|
||||
let canIJoinPublicChannelsInTeam = false;
|
||||
@@ -224,6 +233,16 @@ export const getCurrentChannel = async (database: Database) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getCurrentChannelInfo = async (database: Database) => {
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
if (currentChannelId) {
|
||||
const info = await getChannelInfo(database, currentChannelId);
|
||||
return info;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const observeCurrentChannel = (database: Database) => {
|
||||
return observeCurrentChannelId(database).pipe(
|
||||
switchMap((id) => database.get<ChannelModel>(CHANNEL).query(Q.where('id', id), Q.take(1)).observe().pipe(
|
||||
|
||||
@@ -53,12 +53,12 @@ export const prepareModels = async ({operator, initialTeamId, removeTeams, remov
|
||||
if (chData?.categories?.length) {
|
||||
const categoryModels = prepareCategories(operator, chData.categories);
|
||||
if (categoryModels) {
|
||||
modelPromises.push(...categoryModels);
|
||||
modelPromises.push(categoryModels);
|
||||
}
|
||||
|
||||
const categoryChannelModels = prepareCategoryChannels(operator, chData.categories);
|
||||
if (categoryChannelModels) {
|
||||
modelPromises.push(...categoryChannelModels);
|
||||
modelPromises.push(categoryChannelModels);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const IntroOptions = ({channelId, header, favorite, people, theme}: Props) => {
|
||||
|
||||
const onSetHeader = useCallback(() => {
|
||||
const title = formatMessage({id: 'screens.channel_edit', defaultMessage: 'Edit Channel'});
|
||||
showModal(Screens.CHANNEL_EDIT, title, {channelId});
|
||||
showModal(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId});
|
||||
}, []);
|
||||
|
||||
const onDetails = useCallback(() => {
|
||||
|
||||
345
app/screens/create_or_edit_channel/channel_info_form.tsx
Normal file
345
app/screens/create_or_edit_channel/channel_info_form.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useRef, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
LayoutChangeEvent,
|
||||
TextInput,
|
||||
TouchableWithoutFeedback,
|
||||
StatusBar,
|
||||
View,
|
||||
NativeSyntheticEvent,
|
||||
NativeScrollEvent,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import ErrorText from '@components/error_text';
|
||||
import FloatingTextInput from '@components/floating_text_input_label';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import SectionItem from '@components/section_item';
|
||||
import {General, Channel} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useHeaderHeight from '@hooks/header';
|
||||
import {t} from '@i18n';
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
paddingVertical: 30,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
errorContainer: {
|
||||
width: '100%',
|
||||
},
|
||||
errorWrapper: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 14,
|
||||
height: 40,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
loading: {
|
||||
height: 20,
|
||||
width: 20,
|
||||
color: theme.centerChannelBg,
|
||||
},
|
||||
textInput: {
|
||||
marginTop: 30,
|
||||
},
|
||||
helpText: {
|
||||
...typography('Body', 400, 'Regular'),
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginTop: 10,
|
||||
},
|
||||
headerHelpText: {
|
||||
zIndex: -1,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
channelType?: string;
|
||||
displayName: string;
|
||||
onDisplayNameChange: (text: string) => void;
|
||||
editing: boolean;
|
||||
error?: string | object;
|
||||
header: string;
|
||||
onHeaderChange: (text: string) => void;
|
||||
onTypeChange: (type: ChannelType) => void;
|
||||
purpose: string;
|
||||
onPurposeChange: (text: string) => void;
|
||||
saving: boolean;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export default function ChannelInfoForm({
|
||||
channelType,
|
||||
displayName,
|
||||
onDisplayNameChange,
|
||||
editing,
|
||||
error,
|
||||
header,
|
||||
onHeaderChange,
|
||||
onTypeChange,
|
||||
purpose,
|
||||
onPurposeChange,
|
||||
saving,
|
||||
type,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const isTablet = useIsTablet();
|
||||
const headerHeight = useHeaderHeight(false, false, false);
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const nameInput = useRef<TextInput>(null);
|
||||
const purposeInput = useRef<TextInput>(null);
|
||||
const headerInput = useRef<TextInput>(null);
|
||||
|
||||
const scrollViewRef = useRef<KeyboardAwareScrollView>();
|
||||
|
||||
const updateScrollTimeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const [keyboardVisible, setKeyBoardVisible] = useState<boolean>(false);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
|
||||
const [headerPosition, setHeaderPosition] = useState<number>(0);
|
||||
|
||||
const optionalText = formatMessage({id: t('channel_modal.optional'), defaultMessage: '(optional)'});
|
||||
const labelDisplayName = formatMessage({id: t('channel_modal.name'), defaultMessage: 'Name'});
|
||||
const labelPurpose = formatMessage({id: t('channel_modal.purpose'), defaultMessage: 'Purpose'}) + ' ' + optionalText;
|
||||
const labelHeader = formatMessage({id: t('channel_modal.header'), defaultMessage: 'Header'}) + ' ' + optionalText;
|
||||
|
||||
const placeholderDisplayName = formatMessage({id: t('channel_modal.nameEx'), defaultMessage: 'Bugs, Marketing'});
|
||||
const placeholderPurpose = formatMessage({id: t('channel_modal.purposeEx'), defaultMessage: 'A channel to file bugs and improvements'});
|
||||
const placeholderHeader = formatMessage({id: t('channel_modal.headerEx'), defaultMessage: 'Use Markdown to format header text'});
|
||||
|
||||
const makePrivateLabel = formatMessage({id: t('channel_modal.makePrivate.label'), defaultMessage: 'Make Private'});
|
||||
const makePrivateDescription = formatMessage({id: t('channel_modal.makePrivate.description'), defaultMessage: 'When a channel is set to private, only invited team members can access and participate in that channel.'});
|
||||
|
||||
const displayHeaderOnly = channelType === General.DM_CHANNEL || channelType === General.GM_CHANNEL;
|
||||
const showSelector = !displayHeaderOnly && !editing;
|
||||
|
||||
const isPrivate = type === General.PRIVATE_CHANNEL;
|
||||
|
||||
const handlePress = () => {
|
||||
const chtype = isPrivate ? General.OPEN_CHANNEL : General.PRIVATE_CHANNEL;
|
||||
onTypeChange(chtype);
|
||||
};
|
||||
|
||||
const blur = useCallback(() => {
|
||||
nameInput.current?.blur();
|
||||
purposeInput.current?.blur();
|
||||
headerInput.current?.blur();
|
||||
scrollViewRef.current?.scrollToPosition(0, 0, true);
|
||||
}, []);
|
||||
|
||||
const onHeaderLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => {
|
||||
setHeaderPosition(nativeEvent.layout.y);
|
||||
}, []);
|
||||
|
||||
const scrollHeaderToTop = useCallback(() => {
|
||||
if (scrollViewRef?.current) {
|
||||
scrollViewRef.current?.scrollToPosition(0, headerPosition);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onKeyboardDidShow = useCallback((frames: any) => {
|
||||
setKeyBoardVisible(true);
|
||||
if (Platform.OS === 'android') {
|
||||
setKeyboardHeight(frames.endCoordinates.height);
|
||||
}
|
||||
}, [scrollHeaderToTop]);
|
||||
|
||||
const onKeyboardDidHide = useCallback(() => {
|
||||
setKeyBoardVisible(false);
|
||||
setKeyboardHeight(0);
|
||||
}, []);
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const pos = e.nativeEvent.contentOffset.y;
|
||||
if (updateScrollTimeout.current) {
|
||||
clearTimeout(updateScrollTimeout.current);
|
||||
}
|
||||
updateScrollTimeout.current = setTimeout(() => {
|
||||
setScrollPosition(pos);
|
||||
updateScrollTimeout.current = undefined;
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
if (saving) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar/>
|
||||
<Loading
|
||||
style={styles.loading}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<SafeAreaView
|
||||
edges={['bottom', 'left', 'right']}
|
||||
style={styles.errorContainer}
|
||||
>
|
||||
<View style={styles.errorWrapper}>
|
||||
<ErrorText
|
||||
testID='edit_channel_info.error.text'
|
||||
error={error}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
const platformHeaderHeight = headerHeight.defaultHeight + Platform.select({ios: 10, default: headerHeight.defaultHeight + 10});
|
||||
const postInputTop = (headerPosition + scrollPosition + platformHeaderHeight) - keyboardHeight;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={['bottom', 'left', 'right']}
|
||||
style={styles.container}
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
testID={'edit_channel_info.scrollview'}
|
||||
|
||||
// @ts-expect-error legacy ref
|
||||
ref={scrollViewRef}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
onKeyboardDidShow={onKeyboardDidShow}
|
||||
onKeyboardDidHide={onKeyboardDidHide}
|
||||
enableAutomaticScroll={!keyboardVisible}
|
||||
contentContainerStyle={styles.scrollView}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{displayError}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={blur}
|
||||
>
|
||||
<View>
|
||||
{showSelector && (
|
||||
<SectionItem
|
||||
testID='makePrivate'
|
||||
label={makePrivateLabel}
|
||||
description={makePrivateDescription}
|
||||
action={handlePress}
|
||||
actionType={'toggle'}
|
||||
selected={isPrivate}
|
||||
icon={'lock-outline'}
|
||||
/>
|
||||
)}
|
||||
{!displayHeaderOnly && (
|
||||
<>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
label={labelDisplayName}
|
||||
placeholder={placeholderDisplayName}
|
||||
onChangeText={onDisplayNameChange}
|
||||
maxLength={Channel.MAX_CHANNEL_NAME_LENGTH}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
returnKeyType='next'
|
||||
showErrorIcon={false}
|
||||
spellCheck={false}
|
||||
testID='edit_channel_info.displayname.input'
|
||||
value={displayName}
|
||||
ref={nameInput}
|
||||
containerStyle={styles.textInput}
|
||||
theme={theme}
|
||||
/>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
label={labelPurpose}
|
||||
placeholder={placeholderPurpose}
|
||||
onChangeText={onPurposeChange}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
returnKeyType='next'
|
||||
showErrorIcon={false}
|
||||
spellCheck={false}
|
||||
testID='edit_channel_info.purpose.input'
|
||||
value={purpose}
|
||||
ref={purposeInput}
|
||||
containerStyle={styles.textInput}
|
||||
theme={theme}
|
||||
/>
|
||||
<FormattedText
|
||||
style={styles.helpText}
|
||||
id='channel_modal.descriptionHelp'
|
||||
defaultMessage='Describe how this channel should be used.'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
label={labelHeader}
|
||||
placeholder={placeholderHeader}
|
||||
onChangeText={onHeaderChange}
|
||||
multiline={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
returnKeyType='next'
|
||||
showErrorIcon={false}
|
||||
spellCheck={false}
|
||||
testID='edit_channel_info.header.input'
|
||||
value={header}
|
||||
onLayout={onHeaderLayout}
|
||||
ref={headerInput}
|
||||
containerStyle={styles.textInput}
|
||||
theme={theme}
|
||||
/>
|
||||
<FormattedText
|
||||
style={styles.helpText}
|
||||
id='channel_modal.headerHelp'
|
||||
defaultMessage={'Specify text to appear in the channel header beside the channel name. For example, include frequently used links by typing link text [Link Title](http://example.com).'}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAwareScrollView>
|
||||
<View>
|
||||
<Autocomplete
|
||||
postInputTop={postInputTop}
|
||||
updateValue={onHeaderChange}
|
||||
cursorPosition={header.length}
|
||||
value={header}
|
||||
nestedScrollEnabled={true}
|
||||
maxHeightOverride={isTablet ? 200 : undefined}
|
||||
inPost={false}
|
||||
fixedBottomPosition={false}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
253
app/screens/create_or_edit_channel/create_or_edit_channel.tsx
Normal file
253
app/screens/create_or_edit_channel/create_or_edit_channel.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useEffect, useReducer, useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {ImageResource, Navigation} from 'react-native-navigation';
|
||||
|
||||
import {patchChannel as handlePatchChannel, createChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
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 {validateDisplayName} from '@utils/channel';
|
||||
|
||||
import ChannelInfoForm from './channel_info_form';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
|
||||
type Props = {
|
||||
componentId: string;
|
||||
channel?: ChannelModel;
|
||||
channelInfo?: ChannelInfoModel;
|
||||
}
|
||||
|
||||
const CLOSE_BUTTON_ID = 'close-channel';
|
||||
const EDIT_BUTTON_ID = 'update-channel';
|
||||
const CREATE_BUTTON_ID = 'create-channel';
|
||||
|
||||
enum RequestActions {
|
||||
START = 'Start',
|
||||
COMPLETE = 'Complete',
|
||||
FAILURE = 'Failure',
|
||||
}
|
||||
|
||||
interface RequestState {
|
||||
error: string;
|
||||
saving: boolean;
|
||||
}
|
||||
|
||||
interface RequestAction {
|
||||
type: RequestActions;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const close = (componentId: string): void => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal({componentId});
|
||||
};
|
||||
|
||||
const isDirect = (channel?: ChannelModel): boolean => {
|
||||
return channel?.type === General.DM_CHANNEL || channel?.type === General.GM_CHANNEL;
|
||||
};
|
||||
|
||||
const makeCloseButton = (icon: ImageResource) => {
|
||||
return buildNavigationButton(CLOSE_BUTTON_ID, 'close.more_direct_messages.button', icon);
|
||||
};
|
||||
|
||||
const CreateOrEditChannel = ({
|
||||
componentId,
|
||||
channel,
|
||||
channelInfo,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const editing = Boolean(channel);
|
||||
|
||||
const [type, setType] = useState<ChannelType>(channel?.type as ChannelType || General.OPEN_CHANNEL);
|
||||
const [canSave, setCanSave] = useState(false);
|
||||
|
||||
const [displayName, setDisplayName] = useState<string>(channel?.displayName || '');
|
||||
const [purpose, setPurpose] = useState<string>(channelInfo?.purpose || '');
|
||||
const [header, setHeader] = useState<string>(channelInfo?.header || '');
|
||||
|
||||
const [appState, dispatch] = useReducer((state: RequestState, action: RequestAction) => {
|
||||
switch (action.type) {
|
||||
case RequestActions.START:
|
||||
return {
|
||||
error: '',
|
||||
saving: true,
|
||||
};
|
||||
case RequestActions.COMPLETE:
|
||||
return {
|
||||
error: '',
|
||||
saving: false,
|
||||
};
|
||||
case RequestActions.FAILURE:
|
||||
return {
|
||||
error: action.error,
|
||||
saving: false,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}, {
|
||||
error: '',
|
||||
saving: false,
|
||||
});
|
||||
|
||||
const rightButton = useMemo(() => {
|
||||
const base = buildNavigationButton(
|
||||
editing ? EDIT_BUTTON_ID : CREATE_BUTTON_ID,
|
||||
'edit_channel.save.button',
|
||||
undefined,
|
||||
editing ? formatMessage({id: 'mobile.edit_channel', defaultMessage: 'Save'}) : formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}),
|
||||
);
|
||||
base.enabled = canSave;
|
||||
base.showAsAction = 'always';
|
||||
base.color = theme.sidebarHeaderTextColor;
|
||||
return base;
|
||||
}, [editing, theme.sidebarHeaderTextColor, intl, canSave]);
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [rightButton],
|
||||
});
|
||||
}, [rightButton, componentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const icon = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
setButtons(componentId, {
|
||||
leftButtons: [makeCloseButton(icon)],
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
setCanSave(
|
||||
displayName.length >= MIN_CHANNEL_NAME_LENGTH && (
|
||||
displayName !== channel?.displayName ||
|
||||
purpose !== channelInfo?.purpose ||
|
||||
header !== channelInfo?.header ||
|
||||
type !== channel.type
|
||||
),
|
||||
);
|
||||
}, [channel, displayName, purpose, header, type]);
|
||||
|
||||
const isValidDisplayName = useCallback((): boolean => {
|
||||
if (isDirect(channel)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = validateDisplayName(intl, displayName);
|
||||
if (result.error) {
|
||||
dispatch({
|
||||
type: RequestActions.FAILURE,
|
||||
error: result.error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [channel, displayName]);
|
||||
|
||||
const onCreateChannel = useCallback(async () => {
|
||||
dispatch({type: RequestActions.START});
|
||||
Keyboard.dismiss();
|
||||
if (!isValidDisplayName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanSave(false);
|
||||
const createdChannel = await createChannel(serverUrl, displayName, purpose, header, type);
|
||||
if (createdChannel.error) {
|
||||
dispatch({
|
||||
type: RequestActions.FAILURE,
|
||||
error: createdChannel.error as string,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({type: RequestActions.COMPLETE});
|
||||
close(componentId);
|
||||
switchToChannelById(serverUrl, createdChannel.channel!.id, createdChannel.channel!.team_id);
|
||||
}, [serverUrl, type, displayName, header, purpose, isValidDisplayName]);
|
||||
|
||||
const onUpdateChannel = useCallback(async () => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
dispatch({type: RequestActions.START});
|
||||
Keyboard.dismiss();
|
||||
if (!isValidDisplayName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const patchChannel = {
|
||||
id: channel.id,
|
||||
type: channel.type,
|
||||
display_name: isDirect(channel) ? '' : displayName,
|
||||
purpose,
|
||||
header,
|
||||
} as Channel;
|
||||
|
||||
setCanSave(false);
|
||||
const patchedChannel = await handlePatchChannel(serverUrl, patchChannel);
|
||||
if (patchedChannel.error) {
|
||||
dispatch({
|
||||
type: RequestActions.FAILURE,
|
||||
error: patchedChannel.error as string,
|
||||
});
|
||||
return;
|
||||
}
|
||||
dispatch({type: RequestActions.COMPLETE});
|
||||
close(componentId);
|
||||
}, [channel?.id, channel?.type, displayName, header, purpose, isValidDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
const update = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: {buttonId: string}) => {
|
||||
switch (buttonId) {
|
||||
case CLOSE_BUTTON_ID:
|
||||
close(componentId);
|
||||
break;
|
||||
case CREATE_BUTTON_ID:
|
||||
onCreateChannel();
|
||||
break;
|
||||
case EDIT_BUTTON_ID:
|
||||
onUpdateChannel();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
|
||||
return () => {
|
||||
update.remove();
|
||||
};
|
||||
}, [onCreateChannel, onUpdateChannel]);
|
||||
|
||||
return (
|
||||
<ChannelInfoForm
|
||||
error={appState.error}
|
||||
saving={appState.saving}
|
||||
channelType={channel?.type}
|
||||
editing={editing}
|
||||
onTypeChange={setType}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
onDisplayNameChange={setDisplayName}
|
||||
header={header}
|
||||
onHeaderChange={setHeader}
|
||||
purpose={purpose}
|
||||
onPurposeChange={setPurpose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrEditChannel;
|
||||
27
app/screens/create_or_edit_channel/index.tsx
Normal file
27
app/screens/create_or_edit_channel/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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 {observeChannel, observeChannelInfo} from '@queries/servers/channel';
|
||||
|
||||
import CreateOrEditChannel from './create_or_edit_channel';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = {
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & OwnProps) => {
|
||||
const channel = channelId ? observeChannel(database, channelId) : of$(undefined);
|
||||
const channelInfo = channelId ? observeChannelInfo(database, channelId) : of$(undefined);
|
||||
return {
|
||||
channel,
|
||||
channelInfo,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(CreateOrEditChannel));
|
||||
@@ -8,7 +8,7 @@ import {Navigation} from 'react-native-navigation';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {deletePost, editPost} from '@actions/remote/post';
|
||||
import AutoComplete from '@components/autocomplete';
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
@@ -287,7 +287,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<AutoComplete
|
||||
<Autocomplete
|
||||
channelId={post.channelId}
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
nestedScrollEnabled={true}
|
||||
@@ -298,6 +298,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
||||
postInputTop={1}
|
||||
fixedBottomPosition={true}
|
||||
maxHeightOverride={isTablet ? 200 : undefined}
|
||||
inPost={false}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
case Screens.CREATE_OR_EDIT_CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/create_or_edit_channel').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/custom_status').default,
|
||||
|
||||
@@ -573,7 +573,7 @@ export async function dismissAllModals() {
|
||||
}
|
||||
}
|
||||
|
||||
export const buildNavigationButton = (id: string, testID: string, icon?: ImageResource): OptionsTopBarButton => ({
|
||||
export const buildNavigationButton = (id: string, testID: string, icon?: ImageResource, text?: string): OptionsTopBarButton => ({
|
||||
fontSize: 16,
|
||||
fontFamily: 'OpenSans-SemiBold',
|
||||
fontWeight: '600',
|
||||
@@ -581,6 +581,7 @@ export const buildNavigationButton = (id: string, testID: string, icon?: ImageRe
|
||||
icon,
|
||||
showAsAction: 'always',
|
||||
testID,
|
||||
text,
|
||||
});
|
||||
|
||||
export function setButtons(componentId: string, buttons: NavButtons = {leftButtons: [], rightButtons: []}) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {observeUser} from '@app/queries/servers/user';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
import Reactor from './reactor';
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import {Channel, General, Permissions} from '@constants';
|
||||
import {t, DEFAULT_LOCALE} from '@i18n';
|
||||
import {hasPermission} from '@utils/role';
|
||||
|
||||
import {generateId} from '../general';
|
||||
import {cleanUpUrlable} from '../url';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
export function getDirectChannelName(id: string, otherId: string): string {
|
||||
@@ -62,3 +66,49 @@ export function sortChannelsModelByDisplayName(locale: string, a: ChannelModel,
|
||||
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
|
||||
}
|
||||
|
||||
const displayNameValidationMessages = {
|
||||
display_name_required: {
|
||||
id: t('mobile.rename_channel.display_name_required'),
|
||||
defaultMessage: 'Channel name is required',
|
||||
},
|
||||
display_name_maxLength: {
|
||||
id: t('mobile.rename_channel.display_name_maxLength'),
|
||||
defaultMessage: 'Channel name must be less than {maxLength, number} characters',
|
||||
},
|
||||
display_name_minLength: {
|
||||
id: t('mobile.rename_channel.display_name_minLength'),
|
||||
defaultMessage: 'Channel name must be {minLength, number} or more characters',
|
||||
},
|
||||
};
|
||||
|
||||
export const validateDisplayName = (intl: IntlShape, displayName: string): {error: string} => {
|
||||
let errorMessage;
|
||||
switch (true) {
|
||||
case !displayName:
|
||||
errorMessage = intl.formatMessage(displayNameValidationMessages.display_name_required);
|
||||
break;
|
||||
case displayName.length > Channel.MAX_CHANNEL_NAME_LENGTH:
|
||||
errorMessage = intl.formatMessage(
|
||||
displayNameValidationMessages.display_name_maxLength,
|
||||
{maxLength: Channel.MAX_CHANNEL_NAME_LENGTH});
|
||||
break;
|
||||
case displayName.length < Channel.MIN_CHANNEL_NAME_LENGTH:
|
||||
errorMessage = intl.formatMessage(
|
||||
displayNameValidationMessages.display_name_minLength,
|
||||
{minLength: Channel.MIN_CHANNEL_NAME_LENGTH});
|
||||
break;
|
||||
|
||||
default:
|
||||
errorMessage = '';
|
||||
}
|
||||
return {error: errorMessage};
|
||||
};
|
||||
|
||||
export function generateChannelNameFromDisplayName(displayName: string) {
|
||||
let name = cleanUpUrlable(displayName);
|
||||
if (name === '') {
|
||||
name = generateId();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ export async function canManageChannelMembers(post: PostModel, user: UserModel)
|
||||
const rolesArray = [...user.roles.split(' ')];
|
||||
const channel = await post.channel.fetch() as ChannelModel | undefined;
|
||||
|
||||
if (!channel || channel.deleteAt !== 0 || [General.DM_CHANNEL, General.GM_CHANNEL].includes(channel.type as any) || channel.name === General.DEFAULT_CHANNEL) {
|
||||
const directTypes: string[] = [General.DM_CHANNEL, General.GM_CHANNEL];
|
||||
if (!channel || channel.deleteAt !== 0 || directTypes.includes(channel.type) || channel.name === General.DEFAULT_CHANNEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,17 @@
|
||||
"channel": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_loader.someone": "Someone",
|
||||
"channel_modal.descriptionHelp": "Describe how this channel should be used.",
|
||||
"channel_modal.header": "Header",
|
||||
"channel_modal.headerEx": "Use Markdown to format header text",
|
||||
"channel_modal.headerHelp": "Specify text to appear in the channel header beside the channel name. For example, include frequently used links by typing link text [Link Title](http://example.com).",
|
||||
"channel_modal.makePrivate.": "Make Private",
|
||||
"channel_modal.makePrivate.description": "When a channel is set to private, only invited team members can access and participate in that channel",
|
||||
"channel_modal.name": "Name",
|
||||
"channel_modal.nameEx": "Bugs, Marketing",
|
||||
"channel_modal.optional": "(optional)",
|
||||
"channel_modal.purpose": "Purpose",
|
||||
"channel_modal.purposeEx": "A channel to file bugs and improvements",
|
||||
"combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.",
|
||||
"combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.",
|
||||
"combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.",
|
||||
@@ -213,6 +223,7 @@
|
||||
"mobile.components.select_server_view.proceed": "Proceed",
|
||||
"mobile.create_channel": "Create",
|
||||
"mobile.create_channel.public": "New Public Channel",
|
||||
"mobile.create_channel.title": "New channel",
|
||||
"mobile.create_direct_message.add_more": "You can add {remaining, number} more users",
|
||||
"mobile.create_direct_message.cannot_add_more": "You cannot add more users",
|
||||
"mobile.create_direct_message.one_more": "You can add 1 more user",
|
||||
@@ -230,6 +241,7 @@
|
||||
"mobile.downloader.disabled_title": "Download disabled",
|
||||
"mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n",
|
||||
"mobile.downloader.failed_title": "Download failed",
|
||||
"mobile.edit_channel": "Save",
|
||||
"mobile.edit_post.delete_question": "Are you sure you want to delete this Post?",
|
||||
"mobile.edit_post.delete_title": "Confirm Post Delete",
|
||||
"mobile.edit_post.error": "There was a problem editing this message. Please try again.",
|
||||
@@ -325,6 +337,9 @@
|
||||
"mobile.push_notification_reply.button": "Send",
|
||||
"mobile.push_notification_reply.placeholder": "Write a reply...",
|
||||
"mobile.push_notification_reply.title": "Reply",
|
||||
"mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters",
|
||||
"mobile.rename_channel.display_name_minLength": "Channel name must be {minLength, number} or more characters",
|
||||
"mobile.rename_channel.display_name_required": "Channel name is required",
|
||||
"mobile.request.invalid_request_method": "Invalid request method",
|
||||
"mobile.request.invalid_response": "Received invalid response from the server.",
|
||||
"mobile.reset_status.alert_cancel": "Cancel",
|
||||
|
||||
Reference in New Issue
Block a user