Refactor Android Share extension (js) (#4893)
* Refactor Android Share extension (js) * Feedback review
This commit is contained in:
parent
cff81f168e
commit
043b3a0e8e
54 changed files with 2135 additions and 2161 deletions
|
|
@ -17,9 +17,4 @@ public class ShareActivity extends ReactActivity {
|
|||
MainApplication app = (MainApplication) this.getApplication();
|
||||
app.sharedExtensionIsOpened = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -743,7 +743,7 @@ export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
|||
};
|
||||
}
|
||||
|
||||
function loadSidebar(data) {
|
||||
export function loadSidebar(data) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {channels, channelMembers} = data;
|
||||
|
|
@ -752,6 +752,8 @@ function loadSidebar(data) {
|
|||
if (sidebarActions.length) {
|
||||
dispatch(batchActions(sidebarActions, 'BATCH_LOAD_SIDEBAR'));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ const ViewTypes = keyMirror({
|
|||
SET_LAST_UPGRADE_CHECK: null,
|
||||
|
||||
ADD_RECENT_EMOJI: null,
|
||||
EXTENSION_SELECTED_TEAM_ID: null,
|
||||
ANNOUNCEMENT_BANNER: null,
|
||||
|
||||
INCREMENT_EMOJI_PICKER_PAGE: null,
|
||||
|
|
|
|||
3
app/mm-redux/types/module.d.ts
vendored
3
app/mm-redux/types/module.d.ts
vendored
|
|
@ -3,4 +3,5 @@
|
|||
declare module 'gfycat-sdk';
|
||||
declare module 'remote-redux-devtools';
|
||||
declare module 'redux-action-buffer';
|
||||
declare module 'react-intl';
|
||||
declare module 'react-intl';
|
||||
declare module 'react-native-local-auth';
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
function selectedTeamId(state = '', action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.EXTENSION_SELECTED_TEAM_ID: {
|
||||
return action.data;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
selectedTeamId,
|
||||
});
|
||||
|
|
@ -6,7 +6,6 @@ import {combineReducers} from 'redux';
|
|||
import announcement from './announcement';
|
||||
import channel from './channel';
|
||||
import clientUpgrade from './client_upgrade';
|
||||
import extension from './extension';
|
||||
import i18n from './i18n';
|
||||
import recentEmojis from './recent_emojis';
|
||||
import root from './root';
|
||||
|
|
@ -22,7 +21,6 @@ export default combineReducers({
|
|||
announcement,
|
||||
channel,
|
||||
clientUpgrade,
|
||||
extension,
|
||||
i18n,
|
||||
recentEmojis,
|
||||
root,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import {transformSet} from './utils';
|
|||
* enableThunk - bool - default = true - If true, include the thunk middleware automatically. If false, thunk must be provided as part of additionalMiddleware.
|
||||
*/
|
||||
|
||||
type ReduxStore = {
|
||||
export type ReduxStore = {
|
||||
store: redux.Store;
|
||||
persistor: Persistor;
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ const setTransformer = createTransform(
|
|||
* @param optionalConfig (optional) persist configuration (applies to tests)
|
||||
* @param optionalOptions (optional) middleware configuration (applies to tests)
|
||||
*/
|
||||
export default function configureStore(storage: any, preloadedState: any = {}, optionalConfig: {}, optionalOptions = {}): ReduxStore {
|
||||
export default function configureStore(storage: any, preloadedState: any = {}, optionalConfig: any = {}, optionalOptions = {}): ReduxStore {
|
||||
const defaultOptions: ClientOptions = {
|
||||
enableBuffer: true,
|
||||
enableThunk: true,
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export const isVideo = (file) => {
|
|||
* Get the default extension for a MIME type.
|
||||
*
|
||||
* @param {string} type
|
||||
* @return {boolean|string}
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
export function getExtensionFromMime(type) {
|
||||
|
|
@ -184,7 +184,7 @@ export function getExtensionFromMime(type) {
|
|||
}
|
||||
|
||||
if (!type || typeof type !== 'string') {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: use media-typer
|
||||
|
|
@ -194,7 +194,7 @@ export function getExtensionFromMime(type) {
|
|||
const exts = match && extensions[match[1].toLowerCase()];
|
||||
|
||||
if (!exts || !exts.length) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return exts[0];
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ module.exports = {
|
|||
'@i18n': './app/i18n',
|
||||
'@init': './app/init',
|
||||
'@mm-redux': './app/mm-redux',
|
||||
'@share': './share_extension',
|
||||
'@screens': './app/screens',
|
||||
'@selectors': './app/selectors',
|
||||
'@store': './app/store',
|
||||
|
|
|
|||
2
index.js
2
index.js
|
|
@ -53,7 +53,7 @@ const setFontFamily = () => {
|
|||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const ShareExtension = require('share_extension/android').default;
|
||||
const ShareExtension = require('share_extension/index.tsx').default;
|
||||
const AppRegistry = require('react-native/Libraries/ReactNative/AppRegistry');
|
||||
AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
|
||||
setFontFamily();
|
||||
|
|
|
|||
54
package-lock.json
generated
54
package-lock.json
generated
|
|
@ -5911,6 +5911,27 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz",
|
||||
"integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ=="
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
|
||||
|
|
@ -6065,6 +6086,39 @@
|
|||
"@types/react-native": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-native-video": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-video/-/react-native-video-5.0.3.tgz",
|
||||
"integrity": "sha512-5GbhErfUvilPsZNaSCj6+fOM1UyBMN4QYi8Yb0E4tGzXdnGN4B9ILGe7tivys+EXUmgiWWcXlxQdpaDcKZzlqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"@types/react-native": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-redux": {
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz",
|
||||
"integrity": "sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/shallow-equals": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/shallow-equals/-/shallow-equals-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@
|
|||
"@types/moment-timezone": "0.5.30",
|
||||
"@types/react": "16.9.48",
|
||||
"@types/react-native": "0.63.10",
|
||||
"@types/react-native-video": "5.0.3",
|
||||
"@types/react-redux": "7.1.9",
|
||||
"@types/shallow-equals": "1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "3.10.1",
|
||||
"@typescript-eslint/parser": "3.10.1",
|
||||
|
|
|
|||
106
share_extension/actions/index.ts
Normal file
106
share_extension/actions/index.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {loadSidebar} from '@actions/views/channel';
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
|
||||
import {Role} from '@mm-redux/types/roles';
|
||||
import {getChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
interface TeamChannelsData {
|
||||
channels?: Channel[];
|
||||
channelMembers?: ChannelMembership[];
|
||||
roles?: Role[];
|
||||
sync: boolean;
|
||||
teamId: string;
|
||||
teamChannels: string[];
|
||||
}
|
||||
|
||||
export function loadTeamChannels(teamId?: string) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
if (teamId) {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const actions = [];
|
||||
const data: TeamChannelsData = {
|
||||
sync: true,
|
||||
teamId,
|
||||
teamChannels: getChannelsIdForTeam(state, teamId),
|
||||
};
|
||||
|
||||
if (currentUserId) {
|
||||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||||
try {
|
||||
const [channels, channelMembers] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||
Client4.getMyChannels(teamId, true),
|
||||
Client4.getMyChannelMembers(teamId),
|
||||
]);
|
||||
|
||||
data.channels = channels;
|
||||
data.channelMembers = channelMembers;
|
||||
break;
|
||||
} catch (err) {
|
||||
if (i === MAX_RETRIES) {
|
||||
return {error: err};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.channels) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data,
|
||||
});
|
||||
|
||||
const rolesToLoad = new Set<string>();
|
||||
const members = data.channelMembers;
|
||||
if (members) {
|
||||
for (const member of members) {
|
||||
for (const role of member.roles.split(' ')) {
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rolesToLoad.size > 0) {
|
||||
try {
|
||||
data.roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
|
||||
if (data.roles?.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: data.roles,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log('Could not retrieve channel members roles for the user');
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CHANNELS_FOR_TEAM'));
|
||||
dispatch(loadSidebar(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function getTeamDefaultChannel(teamId: string) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const redirectChannel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
|
||||
return redirectChannel;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
|
||||
import {loadChannelsForTeam} from 'app/actions/views/channel';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function getTeamChannels(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(loadChannelsForTeam(teamId));
|
||||
|
||||
const state = getState();
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const redirectChannel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
|
||||
return redirectChannel?.id;
|
||||
};
|
||||
}
|
||||
|
||||
export function extensionSelectTeamId(teamId) {
|
||||
return {
|
||||
type: ViewTypes.EXTENSION_SELECTED_TEAM_ID,
|
||||
data: teamId,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
TouchableHighlight,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {DirectChannel, GroupChannel, PublicChannel, PrivateChannel} from 'share_extension/common/icons/channel_type';
|
||||
|
||||
const channelTypes = {
|
||||
D: DirectChannel,
|
||||
G: GroupChannel,
|
||||
O: PublicChannel,
|
||||
P: PrivateChannel,
|
||||
};
|
||||
|
||||
export default class ExtensionChannelItem extends PureComponent {
|
||||
static propTypes = {
|
||||
channel: PropTypes.object.isRequired,
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
onPress = preventDoubleTap(() => {
|
||||
const {channel, onSelectChannel} = this.props;
|
||||
requestAnimationFrame(() => {
|
||||
onSelectChannel(channel);
|
||||
});
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
channel,
|
||||
currentChannelId,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isCurrent = channel.id === currentChannelId;
|
||||
let current;
|
||||
|
||||
if (isCurrent) {
|
||||
current = (
|
||||
<View style={style.checkmarkContainer}>
|
||||
<CompassIcon
|
||||
name='check'
|
||||
style={style.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Type = channelTypes[channel.type] || PublicChannel;
|
||||
const icon = <Type/>;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.item}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[style.text]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channel.display_name}
|
||||
</Text>
|
||||
{current}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 45,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
height: 45,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
paddingRight: 5,
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: 5,
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
checkmark: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {CommonActions as NavigationActions} from '@react-navigation/native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
SectionList,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import ExtensionChannelItem from './extension_channel_item';
|
||||
|
||||
const defaultTheme = Preferences.THEMES.default;
|
||||
|
||||
export default class ExtensionTeam extends PureComponent {
|
||||
static propTypes = {
|
||||
directChannels: PropTypes.array,
|
||||
navigation: PropTypes.object.isRequired,
|
||||
privateChannels: PropTypes.array,
|
||||
publicChannels: PropTypes.array,
|
||||
route: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
directChannels: [],
|
||||
privateChannels: [],
|
||||
publicChannels: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
state = {
|
||||
sections: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.buildSections();
|
||||
}
|
||||
|
||||
setSearchBarRef = (ref) => {
|
||||
this.searchBarRef = ref;
|
||||
}
|
||||
|
||||
buildSections = (term) => {
|
||||
const sections = [];
|
||||
let {
|
||||
directChannels: directFiltered,
|
||||
privateChannels: privateFiletered,
|
||||
publicChannels: publicFiltered,
|
||||
} = this.props;
|
||||
|
||||
directFiltered = directFiltered.filter((c) => c.delete_at === 0);
|
||||
privateFiletered = privateFiletered.filter((c) => c.delete_at === 0);
|
||||
publicFiltered = publicFiltered.filter((c) => c.delete_at === 0);
|
||||
|
||||
if (term) {
|
||||
directFiltered = directFiltered.filter((c) => c.display_name.toLowerCase().includes(term.toLowerCase()));
|
||||
privateFiletered = privateFiletered.filter((c) => c.display_name.toLowerCase().includes(term.toLowerCase()));
|
||||
publicFiltered = publicFiltered.filter((c) => c.display_name.toLowerCase().includes(term.toLowerCase()));
|
||||
}
|
||||
|
||||
if (publicFiltered.length) {
|
||||
sections.push({
|
||||
id: 'sidebar.channels',
|
||||
defaultMessage: 'PUBLIC CHANNELS',
|
||||
data: publicFiltered,
|
||||
});
|
||||
}
|
||||
|
||||
if (privateFiletered.length) {
|
||||
sections.push({
|
||||
id: 'sidebar.pg',
|
||||
defaultMessage: 'PRIVATE CHANNELS',
|
||||
data: privateFiletered,
|
||||
});
|
||||
}
|
||||
|
||||
if (directFiltered.length) {
|
||||
sections.push({
|
||||
id: 'sidebar.direct',
|
||||
defaultMessage: 'DIRECT MESSAGES',
|
||||
data: directFiltered,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({sections});
|
||||
};
|
||||
|
||||
handleSelectChannel = async (channel) => {
|
||||
const {navigation, route} = this.props;
|
||||
const backAction = NavigationActions.goBack();
|
||||
|
||||
if (route?.params?.onSelectChannel) {
|
||||
route.params.onSelectChannel(channel.id);
|
||||
}
|
||||
|
||||
navigation.dispatch(backAction);
|
||||
};
|
||||
|
||||
handleSearch = (term) => {
|
||||
this.setState({term}, () => {
|
||||
if (this.throttleTimeout) {
|
||||
clearTimeout(this.throttleTimeout);
|
||||
}
|
||||
|
||||
this.throttleTimeout = setTimeout(() => {
|
||||
this.buildSections(term);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
keyExtractor = (item) => item.id;
|
||||
|
||||
renderBody = (styles) => {
|
||||
const {sections} = this.state;
|
||||
|
||||
if (!sections) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderSearchBar(styles)}
|
||||
<SectionList
|
||||
style={styles.flex}
|
||||
sections={sections}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
keyExtractor={this.keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='on-drag'
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
stickySectionHeadersEnabled={true}
|
||||
scrollEventThrottle={100}
|
||||
windowSize={5}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {route} = this.props;
|
||||
const {currentChannelId} = route.params;
|
||||
|
||||
return (
|
||||
<ExtensionChannelItem
|
||||
channel={item}
|
||||
currentChannelId={currentChannelId}
|
||||
onSelectChannel={this.handleSelectChannel}
|
||||
theme={defaultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
return (
|
||||
<View style={styles.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSearchBar = (styles) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
return (
|
||||
<View style={styles.searchContainer}>
|
||||
<SearchBar
|
||||
ref={this.setSearchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={43}
|
||||
inputStyle={styles.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(defaultTheme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(defaultTheme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(defaultTheme.centerChannelColor, 0.3)}
|
||||
titleCancelColor={defaultTheme.centerChannelColor}
|
||||
onChangeText={this.handleSearch}
|
||||
autoCapitalize='none'
|
||||
value={this.state.term}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {intl} = this.context;
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
const {
|
||||
defaultMessage,
|
||||
id,
|
||||
} = section;
|
||||
|
||||
return (
|
||||
<View style={[styles.titleContainer, {backgroundColor: defaultTheme.centerChannelColor}]}>
|
||||
<View style={{backgroundColor: changeOpacity(defaultTheme.centerChannelBg, 0.6), justifyContent: 'center'}}>
|
||||
<Text style={styles.title}>
|
||||
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
{this.renderBody(styles)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
searchContainer: {
|
||||
paddingBottom: 2,
|
||||
height: 38,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingLeft: 8,
|
||||
},
|
||||
}),
|
||||
},
|
||||
searchBarInput: {
|
||||
backgroundColor: '#fff',
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
},
|
||||
titleContainer: {
|
||||
height: 30,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
height: 30,
|
||||
textAlignVertical: 'center',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
error: {
|
||||
color: theme.errorTextColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {
|
||||
getExtensionSortedDirectChannels,
|
||||
getExtensionSortedPrivateChannels,
|
||||
getExtensionSortedPublicChannels,
|
||||
} from 'share_extension/common/selectors';
|
||||
|
||||
import ExtensionChannels from './extension_channels';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
publicChannels: getExtensionSortedPublicChannels(state),
|
||||
privateChannels: getExtensionSortedPrivateChannels(state),
|
||||
directChannels: getExtensionSortedDirectChannels(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ExtensionChannels);
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class ChannelButton extends PureComponent {
|
||||
static propTypes = {
|
||||
channel: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {onPress, channel, theme} = this.props;
|
||||
const channelName = channel ? channel.display_name : '';
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
{formatMessage({id: 'mobile.share_extension.channel', defaultMessage: 'Channel'})}
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.buttonValue}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
buttonContainer: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderTopWidth: 1,
|
||||
height: 70,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
buttonWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
buttonLabel: {
|
||||
fontSize: 16,
|
||||
marginTop: 16,
|
||||
marginBottom: 3,
|
||||
},
|
||||
buttonValue: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 14,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
|
||||
import ChannelButton from './channel_button';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
channel: getChannel(state, ownProps.channelId),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelButton);
|
||||
|
|
@ -1,795 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {CommonActions as NavigationActions} from '@react-navigation/native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
NativeModules,
|
||||
PermissionsAndroid,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Video from 'react-native-video';
|
||||
import LocalAuth from 'react-native-local-auth';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getFormattedFileSize, lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Loading from '@components/loading';
|
||||
import {MAX_FILE_COUNT, MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {getCurrentServerUrl, getAppCredentials} from '@init/credentials';
|
||||
import {getExtensionFromMime} from '@utils/file';
|
||||
import {setCSRFFromCookie} from '@utils/security';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
import ChannelButton from './channel_button';
|
||||
import TeamButton from './team_button';
|
||||
|
||||
const defaultTheme = Preferences.THEMES.default;
|
||||
const iconForExtension = {
|
||||
csv: 'jumbo-attachment-excel',
|
||||
pdf: 'jumbo-attachment-pdf',
|
||||
ppt: 'jumbo-attachment-powerpoint',
|
||||
pptx: 'jumbo-attachment-powerpoint',
|
||||
xls: 'jumbo-attachment-excel',
|
||||
xlsx: 'jumbo-attachment-excel',
|
||||
zip: 'jumbo-attachment-zip',
|
||||
generic: 'jumbo-attachment-generic',
|
||||
};
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
const INPUT_HEIGHT = 150;
|
||||
|
||||
export default class ExtensionPost extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string,
|
||||
channels: PropTypes.object.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
getTeamChannels: PropTypes.func.isRequired,
|
||||
canUploadFiles: PropTypes.bool.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
navigation: PropTypes.object.isRequired,
|
||||
teamId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
props.navigation.setOptions({
|
||||
title: context.intl.formatMessage({
|
||||
id: 'mobile.extension.title',
|
||||
defaultMessage: 'Share in Mattermost',
|
||||
}),
|
||||
});
|
||||
|
||||
this.state = {
|
||||
channelId: props.channelId,
|
||||
files: [],
|
||||
hasPermission: null,
|
||||
teamId: props.teamId,
|
||||
totalSize: 0,
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.navigation.setOptions({
|
||||
headerLeft: this.leftHeader,
|
||||
});
|
||||
this.auth();
|
||||
}
|
||||
|
||||
auth = async () => {
|
||||
try {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const config = await mattermostManaged.getConfig();
|
||||
|
||||
if (config) {
|
||||
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
|
||||
const vendor = config.vendor || 'Mattermost';
|
||||
|
||||
if (authNeeded) {
|
||||
const isSecured = await mattermostManaged.isDeviceSecure();
|
||||
if (isSecured) {
|
||||
try {
|
||||
await LocalAuth.auth({
|
||||
reason: formatMessage({
|
||||
id: 'mobile.managed.secured_by',
|
||||
defaultMessage: 'Secured by {vendor}',
|
||||
}, {vendor}),
|
||||
fallbackToPasscode: true,
|
||||
suppressEnterPassword: false,
|
||||
});
|
||||
} catch (err) {
|
||||
return this.onClose({nativeEvent: true});
|
||||
}
|
||||
} else {
|
||||
await this.showNotSecuredAlert(vendor);
|
||||
|
||||
return this.onClose({nativeEvent: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return this.initialize();
|
||||
};
|
||||
|
||||
canPost = (error, text, extensionFiles, calculatedSize) => {
|
||||
const {maxFileSize} = this.props;
|
||||
const files = extensionFiles || this.state.files;
|
||||
const totalSize = calculatedSize || this.state.totalSize;
|
||||
const filesOK = files.length ? files.length <= MAX_FILE_COUNT : false;
|
||||
const sizeOK = totalSize ? totalSize <= maxFileSize : false;
|
||||
|
||||
if (!error && ((filesOK && sizeOK) || text.length)) {
|
||||
this.props.navigation.setOptions({headerRight: this.rightHeader});
|
||||
} else {
|
||||
this.props.navigation.setOptions({headerRight: null});
|
||||
}
|
||||
}
|
||||
|
||||
showNotSecuredAlert(vendor) {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.managed.blocked_by',
|
||||
defaultMessage: 'Blocked by {vendor}',
|
||||
}, {vendor}),
|
||||
formatMessage({
|
||||
id: 'mobile.managed.not_secured.android',
|
||||
defaultMessage: 'This device must be secured with a screen lock to use Mattermost.',
|
||||
}),
|
||||
[
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.managed.settings',
|
||||
defaultMessage: 'Go to settings',
|
||||
}),
|
||||
onPress: () => {
|
||||
mattermostManaged.goToSecuritySettings();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.managed.exit',
|
||||
defaultMessage: 'Exit',
|
||||
}),
|
||||
onPress: resolve,
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
{onDismiss: resolve},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getAppCredentials = async () => {
|
||||
try {
|
||||
const url = await getCurrentServerUrl();
|
||||
const credentials = await getAppCredentials();
|
||||
|
||||
if (credentials) {
|
||||
const token = credentials.password;
|
||||
|
||||
if (url && url !== 'undefined' && token && token !== 'undefined') {
|
||||
this.token = token;
|
||||
this.url = url;
|
||||
Client4.setUrl(url);
|
||||
Client4.setToken(token);
|
||||
await setCSRFFromCookie(url);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
getInputRef = (ref) => {
|
||||
this.input = ref;
|
||||
};
|
||||
|
||||
goToChannels = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {navigation} = this.props;
|
||||
const navigateAction = NavigationActions.navigate({
|
||||
name: 'Channels',
|
||||
params: {
|
||||
title: formatMessage({
|
||||
id: 'mobile.routes.selectChannel',
|
||||
defaultMessage: 'Select Channel',
|
||||
}),
|
||||
currentChannelId: this.state.channelId,
|
||||
onSelectChannel: this.handleSelectChannel,
|
||||
},
|
||||
});
|
||||
navigation.dispatch(navigateAction);
|
||||
});
|
||||
|
||||
goToTeams = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {navigation} = this.props;
|
||||
const navigateAction = NavigationActions.navigate({
|
||||
name: 'Teams',
|
||||
params: {
|
||||
title: formatMessage({
|
||||
id: 'mobile.routes.selectTeam',
|
||||
defaultMessage: 'Select Team',
|
||||
}),
|
||||
currentTeamId: this.state.teamId,
|
||||
onSelectTeam: this.handleSelectTeam,
|
||||
},
|
||||
});
|
||||
navigation.dispatch(navigateAction);
|
||||
});
|
||||
|
||||
handleBlur = () => {
|
||||
if (this.input) {
|
||||
this.input.setNativeProps({
|
||||
autoScroll: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
if (this.input) {
|
||||
this.input.setNativeProps({
|
||||
autoScroll: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectChannel = (channelId) => {
|
||||
this.setState({channelId});
|
||||
};
|
||||
|
||||
handleSelectTeam = (teamId, defaultChannelId) => {
|
||||
this.setState({teamId, channelId: defaultChannelId});
|
||||
};
|
||||
|
||||
handleTextChange = (value) => {
|
||||
this.canPost(null, value);
|
||||
this.setState({value});
|
||||
};
|
||||
|
||||
initialize = async () => {
|
||||
await this.getAppCredentials();
|
||||
|
||||
const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
|
||||
let granted;
|
||||
if (!hasPermission) {
|
||||
granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasPermission || granted === PermissionsAndroid.RESULTS.GRANTED) {
|
||||
const data = await ShareExtension.data();
|
||||
return this.loadData(data);
|
||||
}
|
||||
|
||||
return this.setState({hasPermission: false});
|
||||
};
|
||||
|
||||
leftHeader = () => (
|
||||
<TouchableOpacity
|
||||
accessibilityComponentType='button'
|
||||
accessibilityTraits='button'
|
||||
borderless={true}
|
||||
delayPressIn={0}
|
||||
pressColorAndroid='rgba(0, 0, 0, .32)'
|
||||
onPress={this.onClose}
|
||||
>
|
||||
<View style={styles.left}>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
loadData = async (items) => {
|
||||
const {getTeamChannels, teamId} = this.props;
|
||||
if (this.token && this.url) {
|
||||
const text = [];
|
||||
const files = [];
|
||||
let totalSize = 0;
|
||||
let error;
|
||||
|
||||
getTeamChannels(teamId);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
switch (item.type) {
|
||||
case 'text/plain':
|
||||
text.push(item.value);
|
||||
break;
|
||||
default: {
|
||||
let fileSize = {size: 0};
|
||||
const fullPath = item.value;
|
||||
try {
|
||||
fileSize = await RNFetchBlob.fs.stat(fullPath); // eslint-disable-line no-await-in-loop
|
||||
} catch (e) {
|
||||
const {formatMessage} = this.context.intl;
|
||||
error = formatMessage({
|
||||
id: 'mobile.extension.file_error',
|
||||
defaultMessage: 'There was an error reading the file to be shared.\nPlease try again.',
|
||||
});
|
||||
break;
|
||||
}
|
||||
let filename = fullPath.replace(/^.*[\\/]/, '');
|
||||
let extension = filename.split('.').pop();
|
||||
if (extension === filename) {
|
||||
extension = getExtensionFromMime(item.type);
|
||||
filename = `${filename}.${extension}`;
|
||||
}
|
||||
|
||||
totalSize += fileSize.size;
|
||||
files.push({
|
||||
extension,
|
||||
filename,
|
||||
fullPath,
|
||||
mimeType: item.type || lookupMimeType(filename.toLowerCase()),
|
||||
size: getFormattedFileSize(fileSize),
|
||||
type: item.type,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const value = text.join('\n');
|
||||
this.canPost(error, value, files, totalSize);
|
||||
|
||||
this.setState({error, files, value, hasPermission: true, totalSize});
|
||||
}
|
||||
this.setState({loaded: true});
|
||||
};
|
||||
|
||||
onClose = (data) => {
|
||||
ShareExtension.close(data?.nativeEvent ? null : data);
|
||||
};
|
||||
|
||||
onPost = () => {
|
||||
const {channelId, files, value} = this.state;
|
||||
const {currentUserId} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
if (value.length > MAX_MESSAGE_LENGTH_FALLBACK) {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.share_extension.too_long_title',
|
||||
defaultMessage: 'Message is too long',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.share_extension.too_long_message',
|
||||
defaultMessage: 'Character count: {count}/{max}',
|
||||
}, {
|
||||
count: value.length,
|
||||
max: MAX_MESSAGE_LENGTH_FALLBACK,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const data = {
|
||||
channelId,
|
||||
currentUserId,
|
||||
files,
|
||||
token: this.token,
|
||||
url: this.url,
|
||||
value,
|
||||
};
|
||||
|
||||
this.onClose(data);
|
||||
}
|
||||
};
|
||||
|
||||
renderBody = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {channelId, value} = this.state;
|
||||
|
||||
const channel = this.props.channels[channelId];
|
||||
const channelDisplayName = channel?.display_name || ''; //eslint-disable-line camelcase
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={this.getScrollViewRef}
|
||||
contentContainerStyle={styles.scrollView}
|
||||
style={styles.flex}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.getInputRef}
|
||||
autoCapitalize='sentences'
|
||||
multiline={true}
|
||||
onBlur={this.handleBlur}
|
||||
onChangeText={this.handleTextChange}
|
||||
onFocus={this.handleFocus}
|
||||
placeholder={formatMessage({id: 'create_post.write', defaultMessage: 'Write to {channelDisplayName}'}, {channelDisplayName})}
|
||||
placeholderTextColor={changeOpacity(defaultTheme.centerChannelColor, 0.5)}
|
||||
style={styles.input}
|
||||
underlineColorAndroid='transparent'
|
||||
value={value}
|
||||
/>
|
||||
{this.renderFiles()}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
renderChannelButton = () => {
|
||||
const {channelId} = this.state;
|
||||
|
||||
return (
|
||||
<ChannelButton
|
||||
channelId={channelId}
|
||||
onPress={this.goToChannels}
|
||||
theme={defaultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderErrorMessage = (message) => {
|
||||
return (
|
||||
<View
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{message}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderFiles = () => {
|
||||
const {files} = this.state;
|
||||
|
||||
return files.map((file, index) => {
|
||||
let component;
|
||||
|
||||
if (file.type.startsWith('image')) {
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<FastImage
|
||||
source={{uri: file.fullPath, isStatic: true}}
|
||||
resizeMode='cover'
|
||||
style={styles.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (file.type.startsWith('video')) {
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<Video
|
||||
ref={`video-${index}`}
|
||||
style={styles.video}
|
||||
resizeMode='cover'
|
||||
source={{uri: file.fullPath}}
|
||||
volume={0}
|
||||
paused={true}
|
||||
onLoad={() => this.refs[`video-${index}`].seek(0)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
const iconName = iconForExtension[file.extension] || iconForExtension.generic;
|
||||
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.otherContainer}
|
||||
>
|
||||
<View style={styles.otherWrapper}>
|
||||
<View style={styles.fileIcon}>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={32}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.fileContainer}
|
||||
key={`item-${index}`}
|
||||
>
|
||||
{component}
|
||||
<Text
|
||||
ellipsisMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.filename}
|
||||
>
|
||||
{`${file.size} - ${file.filename}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
rightHeader = () => (
|
||||
<TouchableOpacity
|
||||
accessibilityComponentType='button'
|
||||
accessibilityTraits='button'
|
||||
borderless={true}
|
||||
delayPressIn={0}
|
||||
pressColorAndroid='rgba(0, 0, 0, .32)'
|
||||
onPress={this.onPost}
|
||||
disabled={!this.props.canUploadFiles}
|
||||
>
|
||||
<View style={styles.left}>
|
||||
<CompassIcon
|
||||
name='send'
|
||||
size={20}
|
||||
color={defaultTheme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
renderTeamButton = () => {
|
||||
const {teamId} = this.state;
|
||||
|
||||
return (
|
||||
<TeamButton
|
||||
onPress={this.goToTeams}
|
||||
teamId={teamId}
|
||||
theme={defaultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
lengthCounterColor = (count) => {
|
||||
if (count < 0) {
|
||||
return styles.textTooLong;
|
||||
}
|
||||
return styles.textLengthOk;
|
||||
}
|
||||
|
||||
renderMessageLengthRemaining = () => {
|
||||
const {value} = this.state;
|
||||
const messageLengthRemaining = MAX_MESSAGE_LENGTH_FALLBACK - value.length;
|
||||
|
||||
if (value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderStyle = [styles.messageLengthRemaining, this.lengthCounterColor(messageLengthRemaining)];
|
||||
return (
|
||||
<Text style={renderStyle}>
|
||||
{messageLengthRemaining}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {maxFileSize} = this.props;
|
||||
const {error, hasPermission, files, totalSize, loaded, teamId} = this.state;
|
||||
|
||||
if (!loaded) {
|
||||
return (
|
||||
<Loading/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return this.renderErrorMessage(error);
|
||||
}
|
||||
|
||||
if (!teamId && this.token) {
|
||||
const teamRequired = formatMessage({
|
||||
id: 'mobile.extension.team_required',
|
||||
defaultMessage: 'You must belong to a team before you can share files.',
|
||||
});
|
||||
|
||||
return this.renderErrorMessage(teamRequired);
|
||||
}
|
||||
|
||||
if (!this.props.canUploadFiles) {
|
||||
const uploadsDisabled = formatMessage({
|
||||
id: 'mobile.file_upload.disabled',
|
||||
defaultMessage: 'File uploads from mobile are disabled. Please contact your System Admin for more details.',
|
||||
});
|
||||
return this.renderErrorMessage(uploadsDisabled);
|
||||
}
|
||||
|
||||
if (this.token && this.url) {
|
||||
if (hasPermission === false) {
|
||||
const storage = formatMessage({
|
||||
id: 'mobile.extension.permission',
|
||||
defaultMessage: 'Mattermost needs access to the device storage to share files.',
|
||||
});
|
||||
|
||||
return this.renderErrorMessage(storage);
|
||||
} else if (files.length > MAX_FILE_COUNT) {
|
||||
const fileCount = formatMessage({
|
||||
id: 'mobile.extension.file_limit',
|
||||
defaultMessage: 'Sharing is limited to a maximum of 5 files.',
|
||||
});
|
||||
|
||||
return this.renderErrorMessage(fileCount);
|
||||
} else if (totalSize > maxFileSize) {
|
||||
const maxSize = formatMessage({
|
||||
id: 'mobile.extension.max_file_size',
|
||||
defaultMessage: 'File attachments shared in Mattermost must be less than {size}.',
|
||||
}, {size: getFormattedFileSize({size: maxFileSize})});
|
||||
|
||||
return this.renderErrorMessage(maxSize);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
{this.renderBody()}
|
||||
<View style={styles.flex}>
|
||||
{this.renderMessageLengthRemaining()}
|
||||
{this.renderTeamButton()}
|
||||
{this.renderChannelButton()}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const loginNeeded = formatMessage({
|
||||
id: 'mobile.extension.authentication_required',
|
||||
defaultMessage: 'Authentication required: Please first login using the app.',
|
||||
});
|
||||
|
||||
return this.renderErrorMessage(loginNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
left: {
|
||||
alignItems: 'center',
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
width: 50,
|
||||
},
|
||||
closeButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 20,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
wrapper: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
},
|
||||
messageLengthRemaining: {
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
opacity: 0.5,
|
||||
},
|
||||
textLengthOk: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
textTooLong: {
|
||||
color: theme.errorTextColor,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 17,
|
||||
height: INPUT_HEIGHT,
|
||||
marginBottom: 5,
|
||||
textAlignVertical: 'top',
|
||||
width: '100%',
|
||||
},
|
||||
unauthenticatedContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 35,
|
||||
},
|
||||
unauthenticated: {
|
||||
color: theme.errorTextColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
fileContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
height: 48,
|
||||
marginBottom: 10,
|
||||
width: '100%',
|
||||
},
|
||||
filename: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 13,
|
||||
flex: 1,
|
||||
},
|
||||
otherContainer: {
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
height: 48,
|
||||
marginRight: 10,
|
||||
paddingVertical: 10,
|
||||
width: 38,
|
||||
},
|
||||
otherWrapper: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
},
|
||||
fileIcon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
imageContainer: {
|
||||
justifyContent: 'center',
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
height: 48,
|
||||
marginRight: 10,
|
||||
width: 38,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
image: {
|
||||
alignItems: 'center',
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
height: 46,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: 38,
|
||||
},
|
||||
video: {
|
||||
alignItems: 'center',
|
||||
height: 48,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: 38,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, PermissionsAndroid} from 'react-native';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import ExtensionPost from './extension_post';
|
||||
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
jest.spyOn(PermissionsAndroid, 'check').mockReturnValue(PermissionsAndroid.RESULTS.GRANTED);
|
||||
|
||||
jest.mock('@react-navigation/stack/lib/module/views/TouchableItem', () => null);
|
||||
|
||||
jest.mock('app/mattermost_managed', () => ({
|
||||
getConfig: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
|
||||
describe('ExtensionPost', () => {
|
||||
const baseProps = {
|
||||
channelId: 'channel-id',
|
||||
channels: {},
|
||||
currentUserId: 'current-user-id',
|
||||
getTeamChannels: jest.fn(),
|
||||
canUploadFiles: true,
|
||||
maxFileSize: 1024,
|
||||
navigation: {
|
||||
setOptions: jest.fn(),
|
||||
},
|
||||
route: {
|
||||
params: {},
|
||||
},
|
||||
teamId: 'team-id',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ExtensionPost {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.renderErrorMessage = jest.fn();
|
||||
|
||||
const postMessage = (message) => {
|
||||
wrapper.setState({value: message});
|
||||
instance.onPost();
|
||||
};
|
||||
|
||||
test('should show Alert dialog if shared message is longer than maximum length permitted', () => {
|
||||
const longMessage = 'a'.repeat(MAX_MESSAGE_LENGTH + 1);
|
||||
postMessage(longMessage);
|
||||
|
||||
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not show Alert dialog if shared message is within maximum length permitted', () => {
|
||||
const message = 'a'.repeat(MAX_MESSAGE_LENGTH - 1);
|
||||
postMessage(message);
|
||||
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not show Alert dialog if shared message is at maximum length permitted', () => {
|
||||
const exactLengthMessage = 'a'.repeat(MAX_MESSAGE_LENGTH);
|
||||
postMessage(exactLengthMessage);
|
||||
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should render file uploads disabled message when canUploadFiles is false', () => {
|
||||
wrapper.setState({loaded: true});
|
||||
wrapper.setProps({canUploadFiles: false});
|
||||
expect(instance.renderErrorMessage).toHaveBeenCalledWith('File uploads from mobile are disabled. Please contact your System Admin for more details.');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getAllChannels, getCurrentChannel, getDefaultChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getConfig, canUploadFilesOnMobile} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
import {getTeamChannels} from 'share_extension/android/actions';
|
||||
import {getAllowedServerMaxFileSize} from 'app/utils/file';
|
||||
|
||||
import ExtensionPost from './extension_post';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const config = getConfig(state);
|
||||
|
||||
let channel = getCurrentChannel(state);
|
||||
if (channel && (channel.delete_at !== 0)) {
|
||||
channel = getDefaultChannel(state);
|
||||
}
|
||||
|
||||
return {
|
||||
channelId: channel?.id,
|
||||
channels: getAllChannels(state),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
maxFileSize: getAllowedServerMaxFileSize(config),
|
||||
teamId: getCurrentTeamId(state),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = ({
|
||||
getTeamChannels,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ExtensionPost);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTeam} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import TeamButton from './team_button';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
team: getTeam(state, ownProps.teamId),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TeamButton);
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class TeamButton extends PureComponent {
|
||||
static propTypes = {
|
||||
team: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {onPress, team, theme} = this.props;
|
||||
const teamName = team ? team.display_name : '';
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
{formatMessage({id: 'mobile.share_extension.team', defaultMessage: 'Team'})}
|
||||
</Text>
|
||||
<Text style={styles.buttonValue}>
|
||||
{teamName}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
buttonContainer: {
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderTopWidth: 1,
|
||||
height: 70,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
buttonWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
buttonLabel: {
|
||||
fontSize: 16,
|
||||
marginTop: 16,
|
||||
marginBottom: 3,
|
||||
},
|
||||
buttonValue: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 14,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {CommonActions as NavigationActions} from '@react-navigation/native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import TeamItem from './team_item';
|
||||
|
||||
const defaultTheme = Preferences.THEMES.default;
|
||||
|
||||
export default class ExtensionTeam extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
extensionSelectTeamId: PropTypes.func.isRequired,
|
||||
getTeamChannels: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
navigation: PropTypes.object.isRequired,
|
||||
route: PropTypes.object.isRequired,
|
||||
teamIds: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
teamIds: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
handleSelectTeam = async (teamId) => {
|
||||
const {actions, navigation, route} = this.props;
|
||||
const backAction = NavigationActions.goBack();
|
||||
|
||||
if (route?.params?.onSelectTeam) {
|
||||
this.setState({loading: true});
|
||||
const channelId = await actions.getTeamChannels(teamId);
|
||||
actions.extensionSelectTeamId(teamId);
|
||||
route.params.onSelectTeam(teamId, channelId);
|
||||
}
|
||||
|
||||
navigation.dispatch(backAction);
|
||||
};
|
||||
|
||||
keyExtractor = (item) => item;
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
return (
|
||||
<View style={styles.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {params} = this.props.route;
|
||||
|
||||
return (
|
||||
<TeamItem
|
||||
currentTeamId={params.currentTeamId}
|
||||
onSelectTeam={this.handleSelectTeam}
|
||||
teamId={item}
|
||||
theme={defaultTheme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const styles = getStyleSheet(defaultTheme);
|
||||
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={this.props.teamIds}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='on-drag'
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
scrollEventThrottle={100}
|
||||
windowSize={5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getMySortedTeamIds} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {extensionSelectTeamId, getTeamChannels} from 'share_extension/android/actions';
|
||||
|
||||
import ExtensionTeams from './extension_teams';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
teamIds: getMySortedTeamIds(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
extensionSelectTeamId,
|
||||
getTeamChannels,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ExtensionTeams);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTeam} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import TeamItem from './team_item';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
team: getTeam(state, ownProps.teamId),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TeamItem);
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TeamIcon from '@components/team_icon';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class TeamItem extends PureComponent {
|
||||
static propTypes = {
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
onSelectTeam: PropTypes.func.isRequired,
|
||||
team: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
onPress = preventDoubleTap(() => {
|
||||
const {onSelectTeam, team} = this.props;
|
||||
onSelectTeam(team.id);
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentTeamId,
|
||||
team,
|
||||
theme,
|
||||
} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let current;
|
||||
if (team.id === currentTeamId) {
|
||||
current = (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<CompassIcon
|
||||
name='check'
|
||||
style={styles.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.item}>
|
||||
<TeamIcon
|
||||
teamId={team.id}
|
||||
styleContainer={styles.teamIconContainer}
|
||||
styleText={styles.teamIconText}
|
||||
styleImage={styles.imageContainer}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.text]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{team.display_name}
|
||||
</Text>
|
||||
{current}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 45,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
height: 45,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
paddingRight: 5,
|
||||
},
|
||||
teamIconContainer: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
marginRight: 10,
|
||||
},
|
||||
teamIconText: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
imageContainer: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
checkmark: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
|
||||
import {getMyTeams, getMyTeamMembers, selectTeam} from '@mm-redux/actions/teams';
|
||||
|
||||
import {selectDefaultTeam} from '@actions/views/select_team';
|
||||
import {selectDefaultChannel, loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getTranslations} from '@i18n';
|
||||
import {getCurrentLocale} from '@selectors/i18n';
|
||||
import configureStore from '@store';
|
||||
import getStorage from '@store/mmkv_adapter';
|
||||
import Store from '@store/store';
|
||||
import {waitForHydration} from '@store/utils';
|
||||
|
||||
import {extensionSelectTeamId} from './actions';
|
||||
let Extension;
|
||||
|
||||
export default class ShareApp extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!Extension) {
|
||||
Extension = require('./extension').default;
|
||||
}
|
||||
|
||||
this.state = {init: false};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
if (Store.redux) {
|
||||
this.hydrate();
|
||||
return;
|
||||
}
|
||||
|
||||
getStorage().then(this.hydrate);
|
||||
};
|
||||
|
||||
hydrate = (MMKVStorage) => {
|
||||
if (MMKVStorage) {
|
||||
configureStore(MMKVStorage);
|
||||
}
|
||||
waitForHydration(Store.redux, async () => {
|
||||
await this.initTeamAndChannel(Store.redux);
|
||||
|
||||
this.setState({init: true});
|
||||
});
|
||||
}
|
||||
|
||||
initTeamAndChannel = async ({dispatch, getState}) => {
|
||||
await dispatch(getMyTeams());
|
||||
await dispatch(getMyTeamMembers());
|
||||
|
||||
const {myMembers} = getState().entities.teams;
|
||||
if (Object.keys(myMembers).length === 0) {
|
||||
dispatch(selectTeam(''));
|
||||
dispatch(extensionSelectTeamId(''));
|
||||
} else {
|
||||
await dispatch(selectDefaultTeam());
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
await dispatch(loadChannelsForTeam(currentTeamId));
|
||||
await dispatch(selectDefaultChannel(currentTeamId));
|
||||
dispatch(extensionSelectTeamId(currentTeamId));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.init) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const locale = getCurrentLocale(Store.redux.getState());
|
||||
|
||||
return (
|
||||
<Provider store={Store.redux}>
|
||||
<IntlProvider
|
||||
locale={locale}
|
||||
messages={getTranslations(locale)}
|
||||
>
|
||||
<Extension/>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getAllChannels, getChannelsInTeam, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUser, getUsers} from '@mm-redux/selectors/entities/users';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getLastPostPerChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {
|
||||
getMyPreferences,
|
||||
getTeammateNameDisplaySetting,
|
||||
getVisibleTeammate,
|
||||
getVisibleGroupIds,
|
||||
} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {
|
||||
completeDirectChannelDisplayName,
|
||||
getDirectChannelName,
|
||||
getGroupDisplayNameFromUserIds,
|
||||
getUserIdFromChannelName,
|
||||
isAutoClosed,
|
||||
sortChannelsByDisplayName,
|
||||
} from '@mm-redux/utils/channel_utils';
|
||||
import {createIdsSelector} from '@mm-redux/utils/helpers';
|
||||
|
||||
export const getChannelIdsForExtensionTeam = createIdsSelector(
|
||||
(state) => state.views.extension.selectedTeamId,
|
||||
getChannelsInTeam,
|
||||
(teamId, channelsInTeam) => {
|
||||
return Array.from(channelsInTeam[teamId] || []);
|
||||
},
|
||||
);
|
||||
|
||||
export const getExtensionSortedPublicChannels = createSelector(
|
||||
getCurrentUser,
|
||||
getAllChannels,
|
||||
getMyChannelMemberships,
|
||||
getChannelIdsForExtensionTeam,
|
||||
(currentUser, channels, myMembers, teamChannelIds) => {
|
||||
if (!currentUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const locale = currentUser.locale || 'en';
|
||||
return teamChannelIds.reduce((publicChannels, id) => {
|
||||
if (!myMembers[id]) {
|
||||
return publicChannels;
|
||||
}
|
||||
|
||||
const channel = channels[id];
|
||||
if (channel.type === General.OPEN_CHANNEL) {
|
||||
publicChannels.push(channel);
|
||||
}
|
||||
|
||||
return publicChannels;
|
||||
}, []).sort(sortChannelsByDisplayName.bind(null, locale));
|
||||
},
|
||||
);
|
||||
|
||||
export const getExtensionSortedPrivateChannels = createSelector(
|
||||
getCurrentUser,
|
||||
getAllChannels,
|
||||
getMyChannelMemberships,
|
||||
getChannelIdsForExtensionTeam,
|
||||
(currentUser, channels, myMembers, teamChannelIds) => {
|
||||
if (!currentUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const locale = currentUser.locale || 'en';
|
||||
return teamChannelIds.reduce((privateChannels, id) => {
|
||||
if (!myMembers[id]) {
|
||||
return privateChannels;
|
||||
}
|
||||
|
||||
const channel = channels[id];
|
||||
if (channel.type === General.PRIVATE_CHANNEL) {
|
||||
privateChannels.push(channel);
|
||||
}
|
||||
|
||||
return privateChannels;
|
||||
}, []).sort(sortChannelsByDisplayName.bind(null, locale));
|
||||
},
|
||||
);
|
||||
|
||||
export const getExtensionSortedDirectChannels = createSelector(
|
||||
getCurrentUser,
|
||||
getUsers,
|
||||
(state) => state.entities.users.profilesInChannel,
|
||||
getAllChannels,
|
||||
getVisibleTeammate,
|
||||
getVisibleGroupIds,
|
||||
getTeammateNameDisplaySetting,
|
||||
getConfig,
|
||||
getMyPreferences,
|
||||
getLastPostPerChannel,
|
||||
(currentUser, profiles, profilesInChannel, channels, teammates, groupIds, settings, config, preferences, lastPosts) => {
|
||||
if (!currentUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const locale = currentUser.locale || 'en';
|
||||
const channelValues = Object.values(channels);
|
||||
const directChannelsIds = [];
|
||||
teammates.reduce((result, teammateId) => {
|
||||
const name = getDirectChannelName(currentUser.id, teammateId);
|
||||
const channel = channelValues.find((c) => c.name === name); //eslint-disable-line max-nested-callbacks
|
||||
if (channel) {
|
||||
const lastPost = lastPosts[channel.id];
|
||||
const otherUser = profiles[getUserIdFromChannelName(currentUser.id, channel.name)];
|
||||
if (!isAutoClosed(config, preferences, channel, lastPost ? lastPost.create_at : 0, otherUser ? otherUser.delete_at : 0)) {
|
||||
result.push(channel.id);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, directChannelsIds);
|
||||
const directChannels = groupIds.filter((id) => {
|
||||
const channel = channels[id];
|
||||
if (channel) {
|
||||
const lastPost = lastPosts[channel.id];
|
||||
return !isAutoClosed(config, preferences, channels[id], lastPost ? lastPost.create_at : 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}).concat(directChannelsIds).map((id) => {
|
||||
const channel = channels[id];
|
||||
if (channel.type === General.GM_CHANNEL) {
|
||||
return completeDirectGroupInfo(currentUser.id, profiles, profilesInChannel, settings, channel);
|
||||
}
|
||||
return completeDirectChannelDisplayName(currentUser.id, profiles, profilesInChannel[id], settings, channel);
|
||||
}).sort(sortChannelsByDisplayName.bind(null, locale));
|
||||
return directChannels;
|
||||
},
|
||||
);
|
||||
|
||||
function completeDirectGroupInfo(currentUserId, profiles, profilesInChannel, teammateNameDisplay, channel) {
|
||||
const profilesIds = profilesInChannel[channel.id];
|
||||
const gm = {...channel};
|
||||
|
||||
if (profilesIds) {
|
||||
return Object.assign(gm, {
|
||||
display_name: getGroupDisplayNameFromUserIds(profilesIds, profiles, currentUserId, teammateNameDisplay),
|
||||
});
|
||||
}
|
||||
|
||||
const usernames = gm.display_name.split(', ');
|
||||
const users = Object.values(profiles);
|
||||
const userIds = [];
|
||||
usernames.forEach((username) => {
|
||||
const u = users.find((p) => p.username === username);
|
||||
if (u) {
|
||||
userIds.push(u.id);
|
||||
}
|
||||
});
|
||||
if (usernames.length === userIds.length) {
|
||||
return Object.assign(gm, {
|
||||
display_name: getGroupDisplayNameFromUserIds(userIds, profiles, currentUserId, teammateNameDisplay),
|
||||
});
|
||||
}
|
||||
|
||||
return channel;
|
||||
}
|
||||
102
share_extension/components/body.tsx
Normal file
102
share_extension/components/body.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useCallback, useImperativeHandle, useRef, useState} from 'react';
|
||||
import {ScrollView, StyleSheet, TextInput} from 'react-native';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import CharsRemaining from './chars_remaining';
|
||||
import Files from './files';
|
||||
|
||||
export interface BodyProps {
|
||||
canPost: (error?: string, text?: string, extensionFiles?: Array<ShareFileInfo>, calculatedSize?: number) => void;
|
||||
files: Array<ShareFileInfo>;
|
||||
initialValue?: string;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
const INPUT_HEIGHT = 150;
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const Body = forwardRef<BodyRef, BodyProps>(({canPost, files, initialValue, placeholder}: BodyProps, ref) => {
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const getValue = () => {
|
||||
return value?.trim();
|
||||
};
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
inputRef.current?.setNativeProps({
|
||||
autoScroll: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
inputRef.current?.setNativeProps({
|
||||
autoScroll: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleTextChange = useCallback((text) => {
|
||||
canPost(undefined, text);
|
||||
setValue(text);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getValue,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollView}
|
||||
ref={scrollViewRef}
|
||||
style={styles.flex}
|
||||
>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
autoCapitalize='sentences'
|
||||
autoCompleteType='off'
|
||||
multiline={true}
|
||||
onBlur={handleBlur}
|
||||
onChangeText={handleTextChange}
|
||||
onFocus={handleFocus}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={styles.input}
|
||||
underlineColorAndroid='transparent'
|
||||
value={value}
|
||||
/>
|
||||
<Files files={files}/>
|
||||
</ScrollView>
|
||||
<CharsRemaining text={value}/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 17,
|
||||
height: INPUT_HEIGHT,
|
||||
marginBottom: 5,
|
||||
textAlignVertical: 'top',
|
||||
width: '100%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
},
|
||||
});
|
||||
|
||||
Body.displayName = 'Body';
|
||||
|
||||
export default Body;
|
||||
89
share_extension/components/channel_button.tsx
Normal file
89
share_extension/components/channel_button.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {StyleSheet, Text, TouchableHighlight, View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import type {Channel} from '@mm-redux/types/channels';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
interface ChannelButtonProps {
|
||||
channel?: Channel | null;
|
||||
intl: typeof intlShape;
|
||||
onSelect: (channel?: Channel | null) => void;
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const ChannelButton = ({channel, intl, onSelect, teamId}: ChannelButtonProps) => {
|
||||
const navigation = useNavigation();
|
||||
const onPress = () => {
|
||||
navigation.navigate('Channels', {
|
||||
title: intl.formatMessage({id: 'mobile.routes.selectChannel', defaultMessage: 'Select Channel'}),
|
||||
currentChannelId: channel?.id,
|
||||
teamId,
|
||||
onSelectChannel,
|
||||
});
|
||||
};
|
||||
const onSelectChannel = (c: Channel) => {
|
||||
onSelect(c);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<FormattedText
|
||||
defaultMessage='Channel'
|
||||
id='mobile.share_extension.channel'
|
||||
style={styles.buttonLabel}
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.buttonValue}
|
||||
>
|
||||
{channel?.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
buttonContainer: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderTopWidth: 1,
|
||||
height: 70,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
buttonWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
buttonLabel: {
|
||||
fontSize: 16,
|
||||
marginTop: 16,
|
||||
marginBottom: 3,
|
||||
},
|
||||
buttonValue: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelButton);
|
||||
100
share_extension/components/channel_item.tsx
Normal file
100
share_extension/components/channel_item.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import {StyleSheet, Text, TouchableHighlight, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import {DirectChannel, GroupChannel, PublicChannel, PrivateChannel} from '@share/components/channel_type';
|
||||
|
||||
interface ChannelItemProps {
|
||||
onSelect: (channel: Channel) => void;
|
||||
selected: boolean;
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const channelTypes: Record<string, ReactNode> = {
|
||||
D: DirectChannel,
|
||||
G: GroupChannel,
|
||||
O: PublicChannel,
|
||||
P: PrivateChannel,
|
||||
};
|
||||
|
||||
const ChannelItem = ({onSelect, selected, channel}: ChannelItemProps) => {
|
||||
const onPress = preventDoubleTap(() => {
|
||||
onSelect(channel);
|
||||
});
|
||||
|
||||
let current;
|
||||
if (selected) {
|
||||
current = (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<CompassIcon
|
||||
name='check'
|
||||
style={styles.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.item}>
|
||||
{channelTypes[channel.type] || PublicChannel}
|
||||
<Text
|
||||
style={[styles.text]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channel.display_name}
|
||||
</Text>
|
||||
{current}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 45,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
height: 45,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
paddingRight: 5,
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: 5,
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
checkmark: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default ChannelItem;
|
||||
|
|
@ -2,16 +2,12 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
const defaultTheme = Preferences.THEMES.default;
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
export function PublicChannel() {
|
||||
return (
|
||||
|
|
@ -57,18 +53,14 @@ export function GroupChannel() {
|
|||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: 16,
|
||||
marginRight: 5,
|
||||
width: 16,
|
||||
},
|
||||
icon: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 16,
|
||||
},
|
||||
};
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
height: 16,
|
||||
marginRight: 5,
|
||||
width: 16,
|
||||
},
|
||||
icon: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
const style = getStyleSheet(defaultTheme);
|
||||
51
share_extension/components/chars_remaining.tsx
Normal file
51
share_extension/components/chars_remaining.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
interface CharsRemainingProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const CharsRemaining = ({text}: CharsRemainingProps) => {
|
||||
const count = text?.trim().length || 0;
|
||||
const messageLengthRemaining = `${count}/${MAX_MESSAGE_LENGTH_FALLBACK}`;
|
||||
const tooLong = MAX_MESSAGE_LENGTH_FALLBACK - count;
|
||||
const textStyle = tooLong < 0 ? styles.textTooLong : styles.textLengthOk;
|
||||
|
||||
const renderStyle = [styles.messageLengthRemaining, textStyle];
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={renderStyle}>
|
||||
{messageLengthRemaining}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'flex-end',
|
||||
top: -10,
|
||||
},
|
||||
messageLengthRemaining: {
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
opacity: 0.5,
|
||||
},
|
||||
textLengthOk: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
textTooLong: {
|
||||
color: theme.errorTextColor,
|
||||
},
|
||||
});
|
||||
|
||||
export default CharsRemaining;
|
||||
47
share_extension/components/close_header_button.tsx
Normal file
47
share_extension/components/close_header_button.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TouchableOpacity, StyleSheet, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
interface CloseHeaderButtonProps {
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const CloseHeaderButton = ({onPress}: CloseHeaderButtonProps) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
accessibilityComponentType='button'
|
||||
accessibilityTraits='button'
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={styles.left}>
|
||||
<CompassIcon
|
||||
name='arrow-left'
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
left: {
|
||||
alignItems: 'center',
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
width: 50,
|
||||
},
|
||||
closeButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 25,
|
||||
},
|
||||
});
|
||||
|
||||
export default CloseHeaderButton;
|
||||
61
share_extension/components/error.tsx
Normal file
61
share_extension/components/error.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
interface ShareErrorProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const ShareError = ({message}: ShareErrorProps) => {
|
||||
let error;
|
||||
if (message) {
|
||||
error = (
|
||||
<Text style={styles.unauthenticated}>
|
||||
{message}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
error = (
|
||||
<FormattedText
|
||||
defaultMessage='Authentication required: Please first login using the app.'
|
||||
id='mobile.extension.authentication_required'
|
||||
style={styles.unauthenticated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{error}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 35,
|
||||
},
|
||||
unauthenticated: {
|
||||
color: theme.errorTextColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default ShareError;
|
||||
91
share_extension/components/files.tsx
Normal file
91
share_extension/components/files.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import ImageFile from './image_file';
|
||||
import OtherFile from './other_file';
|
||||
import VideoFile from './video_file';
|
||||
|
||||
interface FilesProps {
|
||||
files: Array<ShareFileInfo>;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const Files = ({files}: FilesProps) => {
|
||||
const elements = files.map((file, index) => {
|
||||
let component;
|
||||
const key = `item-${index}`;
|
||||
|
||||
if (file.type.startsWith('image')) {
|
||||
component = (
|
||||
<ImageFile
|
||||
key={key}
|
||||
uri={file.fullPath}
|
||||
/>
|
||||
);
|
||||
} else if (file.type.startsWith('video')) {
|
||||
component = (
|
||||
<VideoFile
|
||||
key={key}
|
||||
uri={file.fullPath}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<OtherFile
|
||||
extension={file.extension}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
key={key}
|
||||
>
|
||||
{component}
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.filename}
|
||||
>
|
||||
{`${file.size} - ${file.filename}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{elements}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
height: 48,
|
||||
marginBottom: 10,
|
||||
width: '100%',
|
||||
},
|
||||
filename: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 13,
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default Files;
|
||||
45
share_extension/components/image_file.tsx
Normal file
45
share_extension/components/image_file.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// 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 FastImage from 'react-native-fast-image';
|
||||
|
||||
interface ImageFileProps {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
const ImageFile = ({uri}: ImageFileProps) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FastImage
|
||||
source={{uri}}
|
||||
resizeMode='cover'
|
||||
style={styles.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
height: 48,
|
||||
marginRight: 10,
|
||||
width: 48,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
image: {
|
||||
alignItems: 'center',
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
height: 46,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: 48,
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageFile;
|
||||
68
share_extension/components/other_file.tsx
Normal file
68
share_extension/components/other_file.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// 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 CompassIcon from '@components/compass_icon';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
interface OtherFileProps {
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
const iconForExtension: Record<string, string> = {
|
||||
csv: 'jumbo-attachment-excel',
|
||||
pdf: 'jumbo-attachment-pdf',
|
||||
ppt: 'jumbo-attachment-powerpoint',
|
||||
pptx: 'jumbo-attachment-powerpoint',
|
||||
xls: 'jumbo-attachment-excel',
|
||||
xlsx: 'jumbo-attachment-excel',
|
||||
zip: 'jumbo-attachment-zip',
|
||||
generic: 'jumbo-attachment-generic',
|
||||
};
|
||||
|
||||
const OtherFile = ({extension}: OtherFileProps) => {
|
||||
let iconName = iconForExtension.generic;
|
||||
if (extension && iconForExtension[extension]) {
|
||||
iconName = iconForExtension[extension];
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.icon}>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={32}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
height: 48,
|
||||
marginRight: 10,
|
||||
paddingVertical: 10,
|
||||
width: 48,
|
||||
},
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
wrapper: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default OtherFile;
|
||||
51
share_extension/components/post_header_button.tsx
Normal file
51
share_extension/components/post_header_button.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TouchableOpacity, StyleSheet, View} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {canUploadFilesOnMobile} from '@mm-redux/selectors/entities/general';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
export const SHARE_EXTENSION_POST_EVENT = 'share-extesion-post-event';
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const PostHeaderButton = () => {
|
||||
const canUploadFiles = useSelector(canUploadFilesOnMobile);
|
||||
const onPress = preventDoubleTap(() => {
|
||||
EventEmitter.emit(SHARE_EXTENSION_POST_EVENT);
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
accessibilityComponentType='button'
|
||||
accessibilityTraits='button'
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
disabled={!canUploadFiles}
|
||||
>
|
||||
<View style={styles.left}>
|
||||
<CompassIcon
|
||||
color={theme.sidebarHeaderTextColor}
|
||||
name='send'
|
||||
size={20}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
left: {
|
||||
alignItems: 'center',
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
width: 50,
|
||||
},
|
||||
});
|
||||
|
||||
export default PostHeaderButton;
|
||||
94
share_extension/components/team_button.tsx
Normal file
94
share_extension/components/team_button.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {StyleSheet, Text, TouchableHighlight, View} from 'react-native';
|
||||
import {useDispatch, useStore} from 'react-redux';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {getMyTeams} from '@mm-redux/actions/teams';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {DispatchFunc} from '@mm-redux/types/actions';
|
||||
import type {Channel} from '@mm-redux/types/channels';
|
||||
import type {Team} from '@mm-redux/types/teams';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {loadTeamChannels, getTeamDefaultChannel} from '@share/actions';
|
||||
|
||||
interface TeamButtonProps {
|
||||
intl: typeof intlShape;
|
||||
onSelect: (team: Team, channel?: Channel | null) => void;
|
||||
team?: Team | null;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const TeamButton = ({intl, onSelect, team}: TeamButtonProps) => {
|
||||
const store = useStore();
|
||||
const dispatch = useDispatch();
|
||||
const navigation = useNavigation();
|
||||
const onPress = () => {
|
||||
dispatch(getMyTeams());
|
||||
navigation.navigate('Teams', {
|
||||
title: intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}),
|
||||
currentTeamId: team?.id,
|
||||
onSelectTeam,
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectTeam = async (t: Team) => {
|
||||
const loadChannels = loadTeamChannels(t.id);
|
||||
const getChannel = getTeamDefaultChannel(t.id);
|
||||
await loadChannels(dispatch as DispatchFunc, store.getState);
|
||||
const defaultChannel = await getChannel(dispatch as DispatchFunc, store.getState);
|
||||
onSelect(t, defaultChannel);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<FormattedText
|
||||
defaultMessage='Team'
|
||||
id='mobile.share_extension.team'
|
||||
style={styles.buttonLabel}
|
||||
/>
|
||||
<Text style={styles.buttonValue}>
|
||||
{team?.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
buttonContainer: {
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderTopWidth: 1,
|
||||
height: 70,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
buttonWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
buttonLabel: {
|
||||
fontSize: 16,
|
||||
marginTop: 16,
|
||||
marginBottom: 3,
|
||||
},
|
||||
buttonValue: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(TeamButton);
|
||||
104
share_extension/components/team_item.tsx
Normal file
104
share_extension/components/team_item.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, TouchableHighlight, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TeamIcon from '@components/team_icon';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {Team} from '@mm-redux/types/teams';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
interface TeamItemProps {
|
||||
onSelect: (team: Team) => void;
|
||||
selected: boolean;
|
||||
team: Team;
|
||||
}
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const TeamItem = ({onSelect, selected, team}: TeamItemProps) => {
|
||||
const onPress = preventDoubleTap(() => {
|
||||
onSelect(team);
|
||||
});
|
||||
|
||||
let current;
|
||||
if (selected) {
|
||||
current = (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<CompassIcon
|
||||
name='check'
|
||||
style={styles.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.item}>
|
||||
<TeamIcon
|
||||
teamId={team.id}
|
||||
styleContainer={styles.teamIconContainer}
|
||||
styleText={styles.teamIconText}
|
||||
styleImage={styles.imageContainer}
|
||||
/>
|
||||
<Text
|
||||
style={styles.text}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{team.display_name}
|
||||
</Text>
|
||||
{current}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 45,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
item: {
|
||||
alignItems: 'center',
|
||||
height: 45,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
paddingRight: 5,
|
||||
},
|
||||
teamIconContainer: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
marginRight: 10,
|
||||
},
|
||||
teamIconText: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
imageContainer: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
checkmark: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default TeamItem;
|
||||
45
share_extension/components/video_file.tsx
Normal file
45
share_extension/components/video_file.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// 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 Video from 'react-native-video';
|
||||
|
||||
interface VideoFileProps {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
const VideoFile = ({uri}: VideoFileProps) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Video
|
||||
style={styles.video}
|
||||
resizeMode='cover'
|
||||
source={{uri}}
|
||||
volume={0}
|
||||
paused={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
height: 48,
|
||||
marginRight: 10,
|
||||
width: 48,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
video: {
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
alignItems: 'center',
|
||||
height: 46,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: 48,
|
||||
},
|
||||
});
|
||||
|
||||
export default VideoFile;
|
||||
55
share_extension/index.tsx
Normal file
55
share_extension/index.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getTranslations} from '@i18n';
|
||||
import {getCurrentLocale} from '@selectors/i18n';
|
||||
import configureStore from '@store';
|
||||
import getStorage from '@store/mmkv_adapter';
|
||||
import Store from '@store/store';
|
||||
import {waitForHydration} from '@store/utils';
|
||||
|
||||
import Extension from './screens/extension';
|
||||
|
||||
const ShareExtension = () => {
|
||||
const [init, setInit] = useState(false);
|
||||
const hydrate = useCallback((mmkvStore?: any) => {
|
||||
if (mmkvStore) {
|
||||
configureStore(mmkvStore);
|
||||
}
|
||||
|
||||
waitForHydration(Store.redux, async () => {
|
||||
setInit(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (Store.redux) {
|
||||
hydrate();
|
||||
} else {
|
||||
getStorage().then(hydrate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!init) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const locale = getCurrentLocale(Store.redux!.getState()) || General.DEFAULT_LOCALE;
|
||||
return (
|
||||
<Provider store={Store.redux!}>
|
||||
<IntlProvider
|
||||
locale={locale}
|
||||
messages={getTranslations(locale)}
|
||||
>
|
||||
<Extension/>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareExtension;
|
||||
229
share_extension/screens/channel_list.tsx
Normal file
229
share_extension/screens/channel_list.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RouteProp, useFocusEffect, useNavigation, useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {BackHandler, SectionList, SectionListData, SectionListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import SearchBar from '@components/search_bar';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import type {Channel} from '@mm-redux/types/channels';
|
||||
import {throttle} from '@utils/general';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import ChannelItem from '@share/components/channel_item';
|
||||
import {getExtensionSortedDirectChannels, getExtensionSortedPrivateChannels, getExtensionSortedPublicChannels} from '@share/selectors';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
interface ChannnelListProps {
|
||||
intl: typeof intlShape;
|
||||
}
|
||||
|
||||
interface SectionData {
|
||||
defaultMessage: string;
|
||||
id: string;
|
||||
data: Channel[];
|
||||
}
|
||||
|
||||
type ChannnelListParams = {
|
||||
Channels: {
|
||||
currentChannelId?: string;
|
||||
onSelectChannel: (channel: Channel) => void;
|
||||
teamId: string;
|
||||
title: string;
|
||||
}
|
||||
}
|
||||
|
||||
type SectionDataHeader = (info: {section: SectionListData<Channel>}) => React.ReactElement | null;
|
||||
|
||||
export type ChannelListRoute = RouteProp<ChannnelListParams, 'Channels'>;
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const ChannelList = ({intl}: ChannnelListProps) => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute<ChannelListRoute>();
|
||||
const {currentChannelId, onSelectChannel, teamId, title} = route.params;
|
||||
const directChannels = useSelector(getExtensionSortedDirectChannels);
|
||||
const privateChannels = useSelector((state: GlobalState) => getExtensionSortedPrivateChannels(state, teamId));
|
||||
const publicChannels = useSelector((state: GlobalState) => getExtensionSortedPublicChannels(state, teamId));
|
||||
const [sections, setSections] = useState<SectionData[]>();
|
||||
const [term, setTerm] = useState<string>();
|
||||
|
||||
const handleSearch = useCallback((text) => {
|
||||
throttle(setTerm(text));
|
||||
}, []);
|
||||
|
||||
const keyExtractor = (item: Channel) => item?.id;
|
||||
const renderItemSeparator = () => (<View style={styles.separator}/>);
|
||||
const renderSectionHeader: SectionDataHeader = ({section}) => (
|
||||
<View style={[styles.titleContainer, {backgroundColor: theme.centerChannelColor}]}>
|
||||
<View style={{backgroundColor: changeOpacity(theme.centerChannelBg, 0.6), justifyContent: 'center'}}>
|
||||
<FormattedText
|
||||
defaultMessage={section.defaultMessage}
|
||||
id={section.id}
|
||||
style={styles.title}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
const renderItem = ({item}: SectionListRenderItemInfo<Channel>) => (
|
||||
<ChannelItem
|
||||
channel={item}
|
||||
onSelect={onSelectChannel}
|
||||
selected={item.id === currentChannelId}
|
||||
/>
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
title,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
const sectionsArray = [];
|
||||
let directFiltered;
|
||||
let privateFiletered;
|
||||
let publicFiltered;
|
||||
|
||||
if (term) {
|
||||
directFiltered = directChannels.filter((c: Channel) => c.delete_at === 0 && c.display_name.toLowerCase().includes(term.toLowerCase()));
|
||||
privateFiletered = privateChannels.filter((c: Channel) => c.delete_at === 0 && c.display_name.toLowerCase().includes(term.toLowerCase()));
|
||||
publicFiltered = publicChannels.filter((c: Channel) => c.delete_at === 0 && c.display_name.toLowerCase().includes(term.toLowerCase()));
|
||||
} else {
|
||||
directFiltered = directChannels.filter((c: Channel) => c.delete_at === 0);
|
||||
privateFiletered = privateChannels.filter((c: Channel) => c.delete_at === 0);
|
||||
publicFiltered = publicChannels.filter((c: Channel) => c.delete_at === 0);
|
||||
}
|
||||
|
||||
if (publicFiltered.length) {
|
||||
sectionsArray.push({
|
||||
id: 'sidebar.channels',
|
||||
defaultMessage: 'PUBLIC CHANNELS',
|
||||
data: publicFiltered,
|
||||
});
|
||||
}
|
||||
|
||||
if (privateFiletered.length) {
|
||||
sectionsArray.push({
|
||||
id: 'sidebar.pg',
|
||||
defaultMessage: 'PRIVATE CHANNELS',
|
||||
data: privateFiletered,
|
||||
});
|
||||
}
|
||||
|
||||
if (directFiltered.length) {
|
||||
sectionsArray.push({
|
||||
id: 'sidebar.direct',
|
||||
defaultMessage: 'DIRECT MESSAGES',
|
||||
data: directFiltered,
|
||||
});
|
||||
}
|
||||
|
||||
setSections(sectionsArray);
|
||||
}, [term]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const onBackPress = () => {
|
||||
navigation.goBack();
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
|
||||
}, []),
|
||||
);
|
||||
|
||||
if (!sections) {
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<View style={styles.searchContainer}>
|
||||
<SearchBar
|
||||
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={43}
|
||||
inputStyle={styles.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.3)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<SectionList
|
||||
style={styles.flex}
|
||||
sections={sections}
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
keyExtractor={keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='on-drag'
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
stickySectionHeadersEnabled={true}
|
||||
scrollEventThrottle={100}
|
||||
windowSize={5}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
searchContainer: {
|
||||
paddingBottom: 2,
|
||||
height: 38,
|
||||
},
|
||||
searchBarInput: {
|
||||
backgroundColor: '#fff',
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
},
|
||||
titleContainer: {
|
||||
height: 30,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
height: 30,
|
||||
textAlignVertical: 'center',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
error: {
|
||||
color: theme.errorTextColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelList);
|
||||
|
|
@ -2,26 +2,35 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Alert, NativeModules} from 'react-native';
|
||||
import {Alert, NativeModules, View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {captureException, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {captureException, initializeSentry, LOGGER_EXTENSION} from '@utils/sentry';
|
||||
|
||||
import Navigation from './navigation';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
const NativeShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
export default class ShareApp extends PureComponent {
|
||||
export default class ShareExtension extends PureComponent {
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return {hasError: error};
|
||||
}
|
||||
|
||||
state = {
|
||||
hasError: undefined,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
initializeSentry();
|
||||
}
|
||||
|
||||
componentDidCatch(error) {
|
||||
componentDidCatch(error: Error) {
|
||||
const {intl, store} = this.context;
|
||||
const {formatMessage} = intl;
|
||||
|
||||
|
|
@ -48,9 +57,20 @@ export default class ShareApp extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
close = () => ShareExtension.close(null)
|
||||
close = () => NativeShareExtension.close(null)
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<View>
|
||||
<FormattedText
|
||||
defaultMessage='Something went wrong'
|
||||
id='mobile.failed_network_action.teams_title'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <Navigation/>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {NavigationContainer} from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import {createStackNavigator} from '@react-navigation/stack';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
import ExtensionChannels from './extension_channels';
|
||||
import ExtensionPost from './extension_post';
|
||||
import ExtensionTeams from './extension_teams';
|
||||
import Channels from './channel_list';
|
||||
import Share from './share';
|
||||
import Teams from './team_list';
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
const defaultNavigationOptions = {
|
||||
headerStyle: {
|
||||
|
|
@ -27,6 +28,7 @@ const defaultNavigationOptions = {
|
|||
margin: 0,
|
||||
},
|
||||
headerTintColor: theme.sidebarHeaderTextColor,
|
||||
headerTopInsetEnabled: false,
|
||||
};
|
||||
|
||||
function RootStack() {
|
||||
|
|
@ -37,15 +39,15 @@ function RootStack() {
|
|||
>
|
||||
<Stack.Screen
|
||||
name='Post'
|
||||
component={ExtensionPost}
|
||||
component={Share}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Teams'
|
||||
component={ExtensionTeams}
|
||||
component={Teams}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Channels'
|
||||
component={ExtensionChannels}
|
||||
component={Channels}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
230
share_extension/screens/share.tsx
Normal file
230
share_extension/screens/share.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useFocusEffect, useNavigation} from '@react-navigation/native';
|
||||
import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {Alert, BackHandler, NativeModules, StyleSheet, View} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {MAX_FILE_COUNT, MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig, canUploadFilesOnMobile} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import type {Team} from '@mm-redux/types/teams';
|
||||
import {getAllowedServerMaxFileSize} from '@utils/file';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import ChannelButton from '@share/components/channel_button';
|
||||
import CloseHeaderButton from '@share/components/close_header_button';
|
||||
import PostHeaderButton, {SHARE_EXTENSION_POST_EVENT} from '@share/components/post_header_button';
|
||||
import Body from '@share/components/body';
|
||||
import TeamButton from '@share/components/team_button';
|
||||
import {isAuthorized, getErrorElement, getSharedItems, permissionEnabled} from '@share/utils';
|
||||
|
||||
interface ShareProps {
|
||||
intl: typeof intlShape;
|
||||
}
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const initialState: ShareState = {
|
||||
loading: true,
|
||||
files: [],
|
||||
};
|
||||
|
||||
const leftButton = () => (
|
||||
<CloseHeaderButton
|
||||
onPress={() => ShareExtension.close(null)}
|
||||
/>
|
||||
);
|
||||
|
||||
const rightButton = () => (<PostHeaderButton/>);
|
||||
|
||||
const Share = ({intl}: ShareProps) => {
|
||||
const navigation = useNavigation();
|
||||
const canUploadFiles = useSelector(canUploadFilesOnMobile);
|
||||
const config = useSelector(getConfig);
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const currentChannel = useSelector(getCurrentChannel);
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
const shareBodyRef = useRef<BodyRef>(null);
|
||||
const [state, setState] = useState<ShareState>(initialState);
|
||||
const [team, setTeam] = useState<Team|undefined|null>(currentTeam);
|
||||
const [channel, setChannel] = useState<Channel|undefined|null>(currentChannel);
|
||||
const maxFileSize = getAllowedServerMaxFileSize(config);
|
||||
|
||||
const showPostButton = (error?: string, text?: string, extensionFiles?: Array<ShareFileInfo>, calculatedSize?: number) => {
|
||||
const files = extensionFiles || state.files;
|
||||
const totalSize = calculatedSize || state.totalSize;
|
||||
const filesOK = files.length ? files.length <= MAX_FILE_COUNT : false;
|
||||
const sizeOK = totalSize ? totalSize <= maxFileSize : false;
|
||||
|
||||
if ((!error && ((filesOK && sizeOK) || text?.length)) && team?.id && channel?.id) {
|
||||
navigation.setOptions({headerRight: rightButton});
|
||||
} else {
|
||||
navigation.setOptions({headerRight: null});
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async (authorized: boolean, hasPermission: boolean) => {
|
||||
const newState: ShareState = {
|
||||
authorized,
|
||||
hasPermission,
|
||||
loading: false,
|
||||
files: [],
|
||||
};
|
||||
|
||||
if (authorized && hasPermission) {
|
||||
const data: Array<ShareItem> = await ShareExtension.data();
|
||||
const {error, files, value, totalSize} = await getSharedItems(data, intl);
|
||||
newState.error = error;
|
||||
newState.files = files;
|
||||
newState.totalSize = totalSize;
|
||||
newState.value = value;
|
||||
}
|
||||
|
||||
showPostButton(newState.error, newState.value, newState.files, newState.totalSize);
|
||||
setState(newState);
|
||||
};
|
||||
|
||||
const postMessage = () => {
|
||||
const {files} = state;
|
||||
const value = shareBodyRef.current?.getValue();
|
||||
const {formatMessage} = intl;
|
||||
const messageLength = value?.length || 0;
|
||||
|
||||
if (messageLength > MAX_MESSAGE_LENGTH_FALLBACK) {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.share_extension.too_long_title',
|
||||
defaultMessage: 'Message is too long',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.share_extension.too_long_message',
|
||||
defaultMessage: 'Character count: {count}/{max}',
|
||||
}, {
|
||||
count: messageLength,
|
||||
max: MAX_MESSAGE_LENGTH_FALLBACK,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const data = {
|
||||
channelId: channel?.id,
|
||||
currentUserId,
|
||||
files,
|
||||
token: Client4.getToken(),
|
||||
url: Client4.getUrl(),
|
||||
value,
|
||||
};
|
||||
|
||||
ShareExtension.close(data);
|
||||
}
|
||||
};
|
||||
|
||||
const selectChannel = (c: Channel) => {
|
||||
setChannel(c);
|
||||
};
|
||||
|
||||
const selectTeam = (t: Team, c?: Channel | null) => {
|
||||
setTeam(t);
|
||||
setChannel(c);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: leftButton,
|
||||
title: intl.formatMessage({
|
||||
id: 'mobile.extension.title',
|
||||
defaultMessage: 'Share in Mattermost',
|
||||
}),
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
isAuthorized(intl).then(async (authorized) => {
|
||||
const hasPermission = await permissionEnabled();
|
||||
loadData(authorized, hasPermission);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.on(SHARE_EXTENSION_POST_EVENT, postMessage);
|
||||
|
||||
return () => EventEmitter.off(SHARE_EXTENSION_POST_EVENT, postMessage);
|
||||
}, [channel, state, team]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const onBackPress = () => {
|
||||
ShareExtension.close(null);
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
|
||||
}, []),
|
||||
);
|
||||
|
||||
const errorElement = getErrorElement(state, canUploadFiles, maxFileSize, team, intl);
|
||||
|
||||
if (errorElement) {
|
||||
return errorElement;
|
||||
}
|
||||
|
||||
const placeholder = intl.formatMessage({
|
||||
id: 'create_post.write',
|
||||
defaultMessage: 'Write to {channelDisplayName}',
|
||||
},
|
||||
{
|
||||
channelDisplayName: channel?.display_name,
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.wrapper}>
|
||||
<Body
|
||||
canPost={showPostButton}
|
||||
files={state.files}
|
||||
initialValue={state.value}
|
||||
placeholder={placeholder}
|
||||
ref={shareBodyRef}
|
||||
/>
|
||||
<View style={styles.flex}>
|
||||
<TeamButton
|
||||
onSelect={selectTeam}
|
||||
team={team}
|
||||
/>
|
||||
<ChannelButton
|
||||
channel={channel}
|
||||
onSelect={selectChannel}
|
||||
teamId={team?.id}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
wrapper: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(Share);
|
||||
97
share_extension/screens/team_list.tsx
Normal file
97
share_extension/screens/team_list.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RouteProp, useFocusEffect, useNavigation, useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useLayoutEffect} from 'react';
|
||||
import {BackHandler, FlatList, StyleSheet, View} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import Loading from '@components/loading';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getMyTeams} from '@mm-redux/selectors/entities/teams';
|
||||
import type {Team} from '@mm-redux/types/teams';
|
||||
import {sortTeamsWithLocale} from '@mm-redux/utils/team_utils';
|
||||
import {getCurrentLocale} from '@selectors/i18n';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import TeamItem from '@share/components/team_item';
|
||||
|
||||
type TeamListParams = {
|
||||
Teams: {
|
||||
currentTeamId?: string;
|
||||
onSelectTeam: (team: Team) => void;
|
||||
title: string;
|
||||
}
|
||||
}
|
||||
|
||||
type TeamListRoute = RouteProp<TeamListParams, 'Teams'>;
|
||||
|
||||
type ListItem = (info: {item: Team}) => React.ReactElement;
|
||||
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
const TeamList = () => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute<TeamListRoute>();
|
||||
const {currentTeamId, onSelectTeam, title} = route.params;
|
||||
const locale: string = useSelector(getCurrentLocale);
|
||||
const teams = useSelector(getMyTeams);
|
||||
teams.sort(sortTeamsWithLocale(locale)).map((t) => t.display_name);
|
||||
|
||||
const keyExtractor = (item: Team) => item?.id;
|
||||
const renderItemSeparator = () => (<View style={styles.separator}/>);
|
||||
const renderItem: ListItem = ({item}) => (
|
||||
<TeamItem
|
||||
selected={item.id === currentTeamId}
|
||||
onSelect={onSelectTeam}
|
||||
team={item}
|
||||
/>
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const onBackPress = () => {
|
||||
navigation.goBack();
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
|
||||
}, []),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
title,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
if (!teams.length) {
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={teams}
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='on-drag'
|
||||
initialNumToRender={10}
|
||||
maxToRenderPerBatch={10}
|
||||
scrollEventThrottle={100}
|
||||
windowSize={5}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default TeamList;
|
||||
81
share_extension/selectors/index.ts
Normal file
81
share_extension/selectors/index.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getAllChannels, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUser, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users';
|
||||
import {getTeammateNameDisplaySetting, getVisibleTeammate, getVisibleGroupIds} from '@mm-redux/selectors/entities/preferences';
|
||||
import {completeDirectChannelDisplayName, getDirectChannelName, sortChannelsByDisplayName} from '@mm-redux/utils/channel_utils';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
export const getExtensionSortedPublicChannels = createSelector(
|
||||
getCurrentUser,
|
||||
getAllChannels,
|
||||
getMyChannelMemberships,
|
||||
(state: GlobalState, teamId: string) => teamId,
|
||||
(user, channels, memberships, teamId) => {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const locale = user.locale || General.DEFAULT_LOCALE;
|
||||
const publicChannels = Object.values(channels).filter((c) => c.team_id === teamId && !c.delete_at && c.type === General.OPEN_CHANNEL && memberships[c.id]);
|
||||
return publicChannels.sort(sortChannelsByDisplayName.bind(null, locale));
|
||||
},
|
||||
);
|
||||
|
||||
export const getExtensionSortedPrivateChannels = createSelector(
|
||||
getCurrentUser,
|
||||
getAllChannels,
|
||||
getMyChannelMemberships,
|
||||
(state: GlobalState, teamId: string) => teamId,
|
||||
(user, channels, memberships, teamId) => {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const locale = user.locale || General.DEFAULT_LOCALE;
|
||||
const privateChannels = Object.values(channels).filter((c) => c.team_id === teamId && !c.delete_at && c.type === General.PRIVATE_CHANNEL && memberships[c.id]);
|
||||
return privateChannels.sort(sortChannelsByDisplayName.bind(null, locale));
|
||||
},
|
||||
);
|
||||
|
||||
export const getExtensionSortedDirectChannels = createSelector(
|
||||
getCurrentUser,
|
||||
getUsers,
|
||||
getUserIdsInChannels,
|
||||
getAllChannels,
|
||||
getVisibleTeammate,
|
||||
getVisibleGroupIds,
|
||||
getTeammateNameDisplaySetting,
|
||||
(user, profiles, profilesInChannel, allChannels, teammates, groupIds, settings) => {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const locale = user.locale || General.DEFAULT_LOCALE;
|
||||
const channels = Object.values(allChannels).filter((c) => !c.team_id);
|
||||
const gms = groupIds.map((id) => {
|
||||
const channel = allChannels[id];
|
||||
const userIdsInChannel = new Set(profilesInChannel[channel.id]);
|
||||
return completeDirectChannelDisplayName(user.id, profiles, userIdsInChannel, settings || '', channel);
|
||||
});
|
||||
|
||||
const dms = teammates.reduce((acc, otherUserId) => {
|
||||
const channelName = getDirectChannelName(user.id, otherUserId);
|
||||
const channel = channels.find((c) => c.name === channelName); //eslint-disable-line max-nested-callbacks
|
||||
const otherUser = profiles[otherUserId];
|
||||
if (channel && otherUser?.delete_at === 0) {
|
||||
const userIdsInChannel = new Set(profilesInChannel[channel.id]);
|
||||
acc.push(completeDirectChannelDisplayName(user.id, profiles, userIdsInChannel, settings || '', channel));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as Channel[]);
|
||||
|
||||
return dms.concat(gms).sort(sortChannelsByDisplayName.bind(null, locale));
|
||||
},
|
||||
);
|
||||
38
share_extension/types/index.d.ts
vendored
Normal file
38
share_extension/types/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
interface BodyRef {
|
||||
getValue(): string | undefined;
|
||||
}
|
||||
|
||||
interface ShareFileInfo {
|
||||
extension?: string;
|
||||
filename: string;
|
||||
fullPath: string;
|
||||
mimeType: string;
|
||||
size: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ShareItem {
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ShareState {
|
||||
authorized?: boolean;
|
||||
error?: string;
|
||||
hasPermission?: boolean;
|
||||
loading: boolean;
|
||||
files: Array<ShareFileInfo>;
|
||||
totalSize?: number;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface ProcessedSharedItems {
|
||||
error?: string;
|
||||
files: Array<ShareFileInfo>;
|
||||
value?: string;
|
||||
totalSize?: number;
|
||||
}
|
||||
|
||||
230
share_extension/utils/index.tsx
Normal file
230
share_extension/utils/index.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Alert, NativeModules, PermissionsAndroid} from 'react-native';
|
||||
import LocalAuth from 'react-native-local-auth';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import Loading from '@components/loading';
|
||||
import {MAX_FILE_COUNT} from '@constants/post_draft';
|
||||
import {getAppCredentials} from '@init/credentials';
|
||||
import type {FileInfo} from '@mm-redux/types/files';
|
||||
import type {Team} from '@mm-redux/types/teams';
|
||||
import {getFormattedFileSize, lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
import {getExtensionFromMime} from '@utils/file';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
import ShareError from '@share/components/error';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
export async function isAuthorized(intl: typeof intlShape): Promise<boolean> {
|
||||
try {
|
||||
const config = await mattermostManaged.getConfig();
|
||||
|
||||
if (config) {
|
||||
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
|
||||
const vendor = config.vendor || 'Mattermost';
|
||||
if (authNeeded) {
|
||||
const isSecured = await mattermostManaged.isDeviceSecure();
|
||||
if (isSecured) {
|
||||
try {
|
||||
await LocalAuth.auth({
|
||||
reason: intl.formatMessage({
|
||||
id: 'mobile.managed.secured_by',
|
||||
defaultMessage: 'Secured by {vendor}',
|
||||
}, {vendor}),
|
||||
fallbackToPasscode: true,
|
||||
suppressEnterPassword: false,
|
||||
});
|
||||
} catch (err) {
|
||||
ShareExtension.close(null);
|
||||
}
|
||||
} else {
|
||||
await showNotSecuredAlert(intl, vendor);
|
||||
|
||||
ShareExtension.close(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return getCredentials();
|
||||
}
|
||||
|
||||
export async function getSharedItems(items: Array<ShareItem>, intl: typeof intlShape): Promise<ProcessedSharedItems> {
|
||||
const text = [];
|
||||
const files: Array<ShareFileInfo> = [];
|
||||
let totalSize = 0;
|
||||
let error;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
switch (item.type) {
|
||||
case 'text/plain':
|
||||
text.push(item.value);
|
||||
break;
|
||||
default: {
|
||||
let fileSize = {size: 0};
|
||||
const fullPath = item.value;
|
||||
try {
|
||||
fileSize = await RNFetchBlob.fs.stat(fullPath); // eslint-disable-line no-await-in-loop
|
||||
} catch (e) {
|
||||
error = intl.formatMessage({
|
||||
id: 'mobile.extension.file_error',
|
||||
defaultMessage: 'There was an error reading the file to be shared.\nPlease try again.',
|
||||
});
|
||||
break;
|
||||
}
|
||||
let filename = fullPath.replace(/^.*[\\/]/, '');
|
||||
let extension = filename.split('.').pop();
|
||||
if (extension === filename) {
|
||||
extension = getExtensionFromMime(item.type);
|
||||
filename = `${filename}.${extension}`;
|
||||
}
|
||||
|
||||
totalSize += fileSize.size;
|
||||
files.push({
|
||||
extension,
|
||||
filename,
|
||||
fullPath,
|
||||
mimeType: item.type || lookupMimeType(filename.toLowerCase()),
|
||||
size: getFormattedFileSize(fileSize as FileInfo),
|
||||
type: item.type,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const value = text.join('\n');
|
||||
|
||||
return {error, files, value, totalSize};
|
||||
}
|
||||
|
||||
export async function permissionEnabled(): Promise<boolean> {
|
||||
const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
|
||||
let granted;
|
||||
if (!hasPermission) {
|
||||
granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
||||
);
|
||||
}
|
||||
|
||||
return (hasPermission || granted === PermissionsAndroid.RESULTS.GRANTED);
|
||||
}
|
||||
|
||||
export function getErrorElement(
|
||||
state: ShareState,
|
||||
canUploadFiles: boolean,
|
||||
maxFileSize: number,
|
||||
team: Team | undefined | null,
|
||||
intl: typeof intlShape,
|
||||
): ReactNode | undefined {
|
||||
if (state.loading) {
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
if (!state.authorized) {
|
||||
return <ShareError/>;
|
||||
}
|
||||
|
||||
if (!team?.id) {
|
||||
const teamRequired = intl.formatMessage({
|
||||
id: 'mobile.extension.team_required',
|
||||
defaultMessage: 'You must belong to a team before you can share files.',
|
||||
});
|
||||
|
||||
return <ShareError message={teamRequired}/>;
|
||||
}
|
||||
|
||||
if (!canUploadFiles) {
|
||||
const uploadsDisabled = intl.formatMessage({
|
||||
id: 'mobile.file_upload.disabled',
|
||||
defaultMessage: 'File uploads from mobile are disabled. Please contact your System Admin for more details.',
|
||||
});
|
||||
|
||||
return <ShareError message={uploadsDisabled}/>;
|
||||
}
|
||||
|
||||
if (state.hasPermission === false) {
|
||||
const storage = intl.formatMessage({
|
||||
id: 'mobile.extension.permission',
|
||||
defaultMessage: 'Mattermost needs access to the device storage to share files.',
|
||||
});
|
||||
|
||||
return <ShareError message={storage}/>;
|
||||
}
|
||||
|
||||
if (state.files.length > MAX_FILE_COUNT) {
|
||||
const fileCount = intl.formatMessage({
|
||||
id: 'mobile.extension.file_limit',
|
||||
defaultMessage: 'Sharing is limited to a maximum of 5 files.',
|
||||
});
|
||||
|
||||
return <ShareError message={fileCount}/>;
|
||||
}
|
||||
|
||||
if ((state.totalSize || 0) > maxFileSize) {
|
||||
const maxSize = intl.formatMessage({
|
||||
id: 'mobile.extension.max_file_size',
|
||||
defaultMessage: 'File attachments shared in Mattermost must be less than {size}.',
|
||||
}, {size: getFormattedFileSize({size: maxFileSize} as FileInfo)});
|
||||
|
||||
return <ShareError message={maxSize}/>;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function getCredentials(): Promise<boolean> {
|
||||
try {
|
||||
const credentials = await getAppCredentials();
|
||||
return Boolean(credentials);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function showNotSecuredAlert(intl: typeof intlShape, vendor: string): Promise<void> {
|
||||
const {formatMessage} = intl;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.managed.blocked_by',
|
||||
defaultMessage: 'Blocked by {vendor}',
|
||||
}, {vendor}),
|
||||
formatMessage({
|
||||
id: 'mobile.managed.not_secured.android',
|
||||
defaultMessage: 'This device must be secured with a screen lock to use Mattermost.',
|
||||
}),
|
||||
[
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.managed.settings',
|
||||
defaultMessage: 'Go to settings',
|
||||
}),
|
||||
onPress: () => {
|
||||
mattermostManaged.goToSecuritySettings();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.managed.exit',
|
||||
defaultMessage: 'Exit',
|
||||
}),
|
||||
onPress: () => resolve(),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
{onDismiss: resolve},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -42,7 +42,9 @@
|
|||
"@mm-redux/*": ["app/mm-redux/*"],
|
||||
"@screens/*": ["app/screens/*"],
|
||||
"@selectors/*": ["app/selectors/*"],
|
||||
"@store/*": ["app/store"],
|
||||
"@share/*": ["share_extension/*"],
|
||||
"@store": ["app/store/index"],
|
||||
"@store/*": ["app/store/*"],
|
||||
"@telemetry/*": ["/app/telemetry/*"],
|
||||
"@utils/*": ["app/utils/*"],
|
||||
"@websocket": ["app/client/websocket"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue