forked from Ivasoft/mattermost-mobile
Compare commits
42 Commits
release-1.
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e36b0468 | ||
|
|
b5b57085e5 | ||
|
|
e082b42947 | ||
|
|
77fdaa9058 | ||
|
|
774f1a1b47 | ||
|
|
192e5093c1 | ||
|
|
52ba404b8e | ||
|
|
8249080304 | ||
|
|
b6c0d47d18 | ||
|
|
031876fb77 | ||
|
|
ac0ac22f39 | ||
|
|
bed81ad514 | ||
|
|
642dd299c6 | ||
|
|
2633060a7f | ||
|
|
50369d0c28 | ||
|
|
4ef308469d | ||
|
|
f2394ba8df | ||
|
|
cc55b03e75 | ||
|
|
82b3dcc1f6 | ||
|
|
13922e3764 | ||
|
|
0df3c7428a | ||
|
|
8e526b61ed | ||
|
|
c93f04a708 | ||
|
|
e3761fc529 | ||
|
|
72fef11496 | ||
|
|
6fdd58b481 | ||
|
|
ad2d126ec0 | ||
|
|
76eb5d06fd | ||
|
|
1e434346ae | ||
|
|
a694122ffd | ||
|
|
0c3bb89832 | ||
|
|
78e6b8d5a3 | ||
|
|
ae7c566375 | ||
|
|
6f260bf4c7 | ||
|
|
ff65b52618 | ||
|
|
2f47d7db2e | ||
|
|
b8e450ba85 | ||
|
|
f2533bd650 | ||
|
|
f9419a7746 | ||
|
|
978c80bef1 | ||
|
|
6e1d8471f7 | ||
|
|
fa9110d9d7 |
@@ -2,6 +2,9 @@
|
||||
|
||||
**Supported Server Versions:** 4.0+
|
||||
|
||||
**Supported iOS versions:** 9.3+
|
||||
**Supported Android versions:** 5.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
|
||||
|
||||
@@ -93,9 +93,9 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 50
|
||||
versionCode 57
|
||||
versionName "1.3.0"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
|
||||
@@ -120,7 +120,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
NotificationPreferencesModule notificationPreferences = NotificationPreferencesModule.getInstance();
|
||||
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
|
||||
|
||||
// First, get a builder initialized with defaults from the core class.
|
||||
final Notification.Builder notification = new Notification.Builder(mContext);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class NotificationPreferences {
|
||||
private static NotificationPreferences instance;
|
||||
|
||||
public final String SHARED_NAME = "NotificationPreferences";
|
||||
public final String SOUND_PREF = "NotificationSound";
|
||||
public final String VIBRATE_PREF = "NotificationVibrate";
|
||||
public final String BLINK_PREF = "NotificationLights";
|
||||
|
||||
private SharedPreferences mSharedPreferences;
|
||||
|
||||
private NotificationPreferences(Context context) {
|
||||
mSharedPreferences = context.getSharedPreferences(SHARED_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static NotificationPreferences getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationPreferences(context);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getNotificationSound() {
|
||||
return mSharedPreferences.getString(SOUND_PREF, null);
|
||||
}
|
||||
|
||||
public boolean getShouldVibrate() {
|
||||
return mSharedPreferences.getBoolean(VIBRATE_PREF, true);
|
||||
}
|
||||
|
||||
public boolean getShouldBlink() {
|
||||
return mSharedPreferences.getBoolean(BLINK_PREF, false);
|
||||
}
|
||||
|
||||
public void setNotificationSound(String soundUri) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putString(SOUND_PREF, soundUri);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public void setShouldVibrate(boolean vibrate) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putBoolean(VIBRATE_PREF, vibrate);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public void setShouldBlink(boolean blink) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putBoolean(BLINK_PREF, blink);
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
@@ -22,18 +21,13 @@ import com.facebook.react.bridge.WritableMap;
|
||||
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
private static NotificationPreferencesModule instance;
|
||||
private final MainApplication mApplication;
|
||||
private SharedPreferences mSharedPreferences;
|
||||
|
||||
private final String SHARED_NAME = "NotificationPreferences";
|
||||
private final String SOUND_PREF = "NotificationSound";
|
||||
private final String VIBRATE_PREF = "NotificationVibrate";
|
||||
private final String BLINK_PREF = "NotificationLights";
|
||||
private NotificationPreferences mNotificationPreference;
|
||||
|
||||
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
Context context = mApplication.getApplicationContext();
|
||||
mSharedPreferences = context.getSharedPreferences(SHARED_NAME, Context.MODE_PRIVATE);
|
||||
mNotificationPreference = NotificationPreferences.getInstance(context);
|
||||
}
|
||||
|
||||
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
|
||||
@@ -76,9 +70,9 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
|
||||
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION);
|
||||
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
|
||||
result.putString("selectedUri", getNotificationSound());
|
||||
result.putBoolean("shouldVibrate", getShouldVibrate());
|
||||
result.putBoolean("shouldBlink", getShouldBlink());
|
||||
result.putString("selectedUri", mNotificationPreference.getNotificationSound());
|
||||
result.putBoolean("shouldVibrate", mNotificationPreference.getShouldVibrate());
|
||||
result.putBoolean("shouldBlink", mNotificationPreference.getShouldBlink());
|
||||
result.putArray("sounds", sounds);
|
||||
|
||||
promise.resolve(result);
|
||||
@@ -97,34 +91,16 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@ReactMethod
|
||||
public void setNotificationSound(String soundUri) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putString(SOUND_PREF, soundUri);
|
||||
editor.commit();
|
||||
mNotificationPreference.setNotificationSound(soundUri);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setShouldVibrate(boolean vibrate) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putBoolean(VIBRATE_PREF, vibrate);
|
||||
editor.commit();
|
||||
mNotificationPreference.setShouldVibrate(vibrate);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setShouldBlink(boolean vibrate) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putBoolean(BLINK_PREF, vibrate);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public String getNotificationSound() {
|
||||
return mSharedPreferences.getString(SOUND_PREF, null);
|
||||
}
|
||||
|
||||
public boolean getShouldVibrate() {
|
||||
return mSharedPreferences.getBoolean(VIBRATE_PREF, true);
|
||||
}
|
||||
|
||||
public boolean getShouldBlink() {
|
||||
return mSharedPreferences.getBoolean(BLINK_PREF, false);
|
||||
public void setShouldBlink(boolean blink) {
|
||||
mNotificationPreference.setShouldBlink(blink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
unfavoriteChannel
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getPosts, getPostsWithRetry, getPostsBefore, getPostsSinceWithRetry, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
|
||||
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
const MAX_POST_TRIES = 3;
|
||||
|
||||
export function loadChannelsIfNecessary(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
|
||||
@@ -139,25 +141,57 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
}
|
||||
|
||||
export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
return (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
|
||||
const postsIds = postsInChannel[channelId];
|
||||
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
const time = Date.now();
|
||||
|
||||
let received;
|
||||
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
|
||||
getPostsWithRetry(channelId)(dispatch, getState);
|
||||
return;
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
} else {
|
||||
const {lastConnectAt} = state.device.websocket;
|
||||
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
|
||||
|
||||
let since;
|
||||
if (lastGetPosts && lastGetPosts < lastConnectAt) {
|
||||
// Since the websocket disconnected, we may have missed some posts since then
|
||||
since = lastGetPosts;
|
||||
} else {
|
||||
// Trust that we've received all posts since the last time the websocket disconnected
|
||||
// so just get any that have changed since the latest one we've received
|
||||
const postsForChannel = postsIds.map((id) => posts[id]);
|
||||
since = getLastCreateAt(postsForChannel);
|
||||
}
|
||||
|
||||
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
|
||||
}
|
||||
|
||||
const postsForChannel = postsIds.map((id) => posts[id]);
|
||||
const latestPostTime = getLastCreateAt(postsForChannel);
|
||||
|
||||
getPostsSinceWithRetry(channelId, latestPostTime)(dispatch, getState);
|
||||
if (received) {
|
||||
dispatch({
|
||||
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
|
||||
channelId,
|
||||
time
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
const posts = await action(dispatch, getState);
|
||||
|
||||
if (posts) {
|
||||
return posts;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadFilesForPostIfNecessary(postId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {files} = getState().entities;
|
||||
@@ -184,34 +218,35 @@ export function loadThreadIfNecessary(rootId, channelId) {
|
||||
export function selectInitialChannel(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {channels, currentChannelId, myMembers} = state.entities.channels;
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
const currentChannel = channels[currentChannelId];
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
|
||||
const lastChannel = channels[lastChannelId];
|
||||
|
||||
const isDMVisible = currentChannel && currentChannel.type === General.DM_CHANNEL &&
|
||||
isDirectChannelVisible(currentUserId, myPreferences, currentChannel);
|
||||
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
|
||||
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
|
||||
|
||||
const isGMVisible = currentChannel && currentChannel.type === General.GM_CHANNEL &&
|
||||
isGroupChannelVisible(myPreferences, currentChannel);
|
||||
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
|
||||
isGroupChannelVisible(myPreferences, lastChannel);
|
||||
|
||||
if (currentChannel && myMembers[currentChannelId] &&
|
||||
(currentChannel.team_id === teamId || isDMVisible || isGMVisible)) {
|
||||
await handleSelectChannel(currentChannelId)(dispatch, getState);
|
||||
if (lastChannelId && myMembers[lastChannelId] &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
|
||||
handleSelectChannel(lastChannelId)(dispatch, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
|
||||
if (channel) {
|
||||
dispatch(setChannelDisplayName(''));
|
||||
await handleSelectChannel(channel.id)(dispatch, getState);
|
||||
handleSelectChannel(channel.id)(dispatch, getState);
|
||||
} else {
|
||||
// Handle case when the default channel cannot be found
|
||||
// so we need to get the first available channel of the team
|
||||
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
|
||||
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
|
||||
dispatch(setChannelDisplayName(''));
|
||||
await handleSelectChannel(firstChannel.id)(dispatch, getState);
|
||||
handleSelectChannel(firstChannel.id)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -220,13 +255,15 @@ export function handleSelectChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
|
||||
selectChannel(channelId)(dispatch, getState);
|
||||
dispatch(setChannelLoading(false));
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
|
||||
teamId: currentTeamId,
|
||||
channelId
|
||||
});
|
||||
getChannelStats(channelId)(dispatch, getState);
|
||||
selectChannel(channelId)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,7 +341,7 @@ export function closeGMChannel(channel) {
|
||||
|
||||
export function refreshChannelWithRetry(channelId) {
|
||||
return (dispatch, getState) => {
|
||||
getPostsWithRetry(channelId)(dispatch, getState);
|
||||
return retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
|
||||
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
|
||||
@@ -12,20 +14,24 @@ import {setChannelDisplayName} from './channel';
|
||||
|
||||
export function handleTeamChange(team, selectChannel = true) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
const state = getState();
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
if (currentTeamId === team.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const actions = [
|
||||
setChannelDisplayName(''),
|
||||
{type: TeamTypes.SELECT_TEAM, data: team.id}
|
||||
];
|
||||
|
||||
if (selectChannel) {
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
|
||||
|
||||
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId});
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions), getState);
|
||||
|
||||
@@ -1,47 +1,39 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ListView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {sortByUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {SectionList} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i;
|
||||
const FROM_REGEX = /\bfrom:\s*(\S*)$/i;
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class AtMention extends Component {
|
||||
export default class AtMention extends PureComponent {
|
||||
static propTypes = {
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
defaultChannel: PropTypes.object.isRequired,
|
||||
autocompleteUsers: PropTypes.object.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
postDraft: PropTypes.string,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
autocompleteUsers: PropTypes.func.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
currentChannelId: PropTypes.string,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
defaultChannel: PropTypes.object,
|
||||
inChannel: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
matchTerm: PropTypes.string,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
outChannel: PropTypes.array,
|
||||
postDraft: PropTypes.string,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
teamMembers: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autocompleteUsers: {},
|
||||
defaultChannel: {},
|
||||
postDraft: '',
|
||||
isSearch: false
|
||||
@@ -50,74 +42,82 @@ export default class AtMention extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const ds = new ListView.DataSource({
|
||||
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
|
||||
rowHasChanged: (r1, r2) => r1 !== r2
|
||||
});
|
||||
const data = {};
|
||||
this.state = {
|
||||
active: false,
|
||||
dataSource: ds.cloneWithRowsAndSections(data)
|
||||
sections: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {isSearch} = nextProps;
|
||||
const regex = isSearch ? FROM_REGEX : AT_MENTION_REGEX;
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
|
||||
|
||||
if (!match || this.state.mentionComplete) {
|
||||
const {inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
|
||||
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
|
||||
// if the term changes but is null or the mention has been completed we render this component as null
|
||||
this.setState({
|
||||
active: false,
|
||||
matchTerm: null,
|
||||
mentionComplete: false
|
||||
mentionComplete: false,
|
||||
sections: []
|
||||
});
|
||||
return;
|
||||
} else if (matchTerm === null) {
|
||||
// if the terms did not change but is null then we don't need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
const matchTerm = isSearch ? match[1] : match[2];
|
||||
if (matchTerm !== this.state.matchTerm) {
|
||||
this.setState({
|
||||
matchTerm
|
||||
});
|
||||
|
||||
if (matchTerm !== this.props.matchTerm) {
|
||||
// if the term changed and we haven't made the request do that first
|
||||
const {currentTeamId, currentChannelId} = this.props;
|
||||
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, currentChannelId);
|
||||
const channelId = isSearch ? '' : currentChannelId;
|
||||
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextProps.requestStatus !== RequestStatus.STARTED) {
|
||||
const membersInChannel = this.filter(nextProps.autocompleteUsers.inChannel, matchTerm) || [];
|
||||
const membersOutOfChannel = this.filter(nextProps.autocompleteUsers.outChannel, matchTerm) || [];
|
||||
|
||||
let data = {};
|
||||
if (requestStatus !== RequestStatus.STARTED &&
|
||||
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)) {
|
||||
// if the request is complete and the term is not null we show the autocomplete
|
||||
const sections = [];
|
||||
if (isSearch) {
|
||||
data = {members: membersInChannel.concat(membersOutOfChannel).sort(sortByUsername)};
|
||||
sections.push({
|
||||
id: 'mobile.suggestion.members',
|
||||
defaultMessage: 'Members',
|
||||
data: teamMembers,
|
||||
key: 'teamMembers'
|
||||
});
|
||||
} else {
|
||||
if (membersInChannel.length > 0) {
|
||||
data = Object.assign({}, data, {inChannel: membersInChannel});
|
||||
if (inChannel.length) {
|
||||
sections.push({
|
||||
id: 'suggestion.mention.members',
|
||||
defaultMessage: 'Channel Members',
|
||||
data: inChannel,
|
||||
key: 'inChannel'
|
||||
});
|
||||
}
|
||||
if (this.checkSpecialMentions(matchTerm) && !isSearch) {
|
||||
data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()});
|
||||
|
||||
if (this.checkSpecialMentions(matchTerm)) {
|
||||
sections.push({
|
||||
id: 'suggestion.mention.special',
|
||||
defaultMessage: 'Special Mentions',
|
||||
data: this.getSpecialMentions(),
|
||||
key: 'special',
|
||||
renderItem: this.renderSpecialMentions
|
||||
});
|
||||
}
|
||||
if (membersOutOfChannel.length > 0) {
|
||||
data = Object.assign({}, data, {notInChannel: membersOutOfChannel});
|
||||
|
||||
if (outChannel.length) {
|
||||
sections.push({
|
||||
id: 'suggestion.mention.nonmembers',
|
||||
defaultMessage: 'Not in Channel',
|
||||
data: outChannel,
|
||||
key: 'outChannel'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel') || data.hasOwnProperty('members'),
|
||||
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
|
||||
sections
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
filter = (profiles, matchTerm) => {
|
||||
const {isSearch} = this.props;
|
||||
return profiles.filter((p) => {
|
||||
return ((p.id !== this.props.currentUserId || isSearch) && (
|
||||
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
|
||||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
|
||||
});
|
||||
keyExtractor = (item) => {
|
||||
return item.id || item;
|
||||
};
|
||||
|
||||
getSpecialMentions = () => {
|
||||
@@ -149,7 +149,7 @@ export default class AtMention extends Component {
|
||||
|
||||
let completedDraft;
|
||||
if (isSearch) {
|
||||
completedDraft = mentionPart.replace(FROM_REGEX, `from: ${mention} `);
|
||||
completedDraft = mentionPart.replace(AT_MENTION_SEARCH_REGEX, `from: ${mention} `);
|
||||
} else {
|
||||
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
}
|
||||
@@ -158,141 +158,63 @@ export default class AtMention extends Component {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
this.setState({
|
||||
active: false,
|
||||
mentionComplete: true
|
||||
});
|
||||
onChangeText(completedDraft, true);
|
||||
this.setState({mentionComplete: true});
|
||||
};
|
||||
|
||||
renderSectionHeader = (sectionData, sectionId) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
const localization = {
|
||||
inChannel: {
|
||||
id: 'suggestion.mention.members',
|
||||
defaultMessage: 'Channel Members'
|
||||
},
|
||||
notInChannel: {
|
||||
id: 'suggestion.mention.nonmembers',
|
||||
defaultMessage: 'Not in Channel'
|
||||
},
|
||||
specialMentions: {
|
||||
id: 'suggestion.mention.special',
|
||||
defaultMessage: 'Special Mentions'
|
||||
},
|
||||
members: {
|
||||
id: 'mobile.suggestion.members',
|
||||
defaultMessage: 'Members'
|
||||
}
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.section}>
|
||||
<FormattedText
|
||||
id={localization[sectionId].id}
|
||||
defaultMessage={localization[sectionId].defaultMessage}
|
||||
style={style.sectionText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<AutocompleteSectionHeader
|
||||
id={section.id}
|
||||
defaultMessage={section.defaultMessage}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderRow = (data, sectionId) => {
|
||||
if (sectionId === 'specialMentions') {
|
||||
return this.renderSpecialMentions(data);
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
const hasFullName = data.first_name.length > 0 && data.last_name.length > 0;
|
||||
|
||||
renderItem = ({item}) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => this.completeMention(data.username)}
|
||||
style={style.row}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
user={data}
|
||||
theme={this.props.theme}
|
||||
size={20}
|
||||
status={null}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${data.username}`}</Text>
|
||||
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
|
||||
{hasFullName && <Text style={style.rowFullname}>{`${data.first_name} ${data.last_name}`}</Text>}
|
||||
</TouchableOpacity>
|
||||
<AtMentionItem
|
||||
onPress={this.completeMention}
|
||||
userId={item}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSpecialMentions = (data) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
renderSpecialMentions = ({item}) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => this.completeMention(data.completeHandle)}
|
||||
style={style.row}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<Icon
|
||||
name='users'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.textWrapper}>
|
||||
<Text style={style.rowUsername}>{`@${data.completeHandle}`}</Text>
|
||||
<Text style={style.rowUsername}>{' - '}</Text>
|
||||
<FormattedText
|
||||
id={data.id}
|
||||
defaultMessage={data.defaultMessage}
|
||||
values={data.values}
|
||||
style={[style.rowFullname, {flex: 1}]}
|
||||
/>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<SpecialMentionItem
|
||||
completeHandle={item.completeHandle}
|
||||
defaultMessage={item.defaultMessage}
|
||||
id={item.id}
|
||||
onPress={this.completeMention}
|
||||
theme={this.props.theme}
|
||||
values={item.values}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {autocompleteUsers, requestStatus} = this.props;
|
||||
if (!this.state.active && (requestStatus !== RequestStatus.STARTED || requestStatus !== RequestStatus.SUCCESS)) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
const {isSearch, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
// If we are not in an active state or the mention has been completed return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
if (
|
||||
!autocompleteUsers.inChannel &&
|
||||
!autocompleteUsers.outChannel &&
|
||||
requestStatus === RequestStatus.STARTED
|
||||
) {
|
||||
return (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
style={style.sectionText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<ListView
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={style.listView}
|
||||
enableEmptySections={true}
|
||||
dataSource={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, isSearch ? style.search : null]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderRow={this.renderRow}
|
||||
renderFooter={this.renderFooter}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
initialNumToRender={10}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -300,72 +222,11 @@ export default class AtMention extends Component {
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
section: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
sectionText: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
paddingVertical: 7
|
||||
},
|
||||
sectionWrapper: {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
loading: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 20,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 0
|
||||
},
|
||||
row: {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
fontSize: 14
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
width: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
rowUsername: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
rowFullname: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6
|
||||
},
|
||||
textWrapper: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
paddingRight: 8
|
||||
search: {
|
||||
height: 250
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,21 +4,29 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {autocompleteUsers} from 'mattermost-redux/actions/users';
|
||||
import {getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getProfilesInCurrentChannel, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentChannelId, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
filterMembersInChannel,
|
||||
filterMembersNotInChannel,
|
||||
filterMembersInCurrentTeam,
|
||||
getMatchTermForAtMention
|
||||
} from 'app/selectors/autocomplete';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
const {cursorPosition, isSearch, rootId} = ownProps;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
let postDraft;
|
||||
if (ownProps.isSearch) {
|
||||
let postDraft = '';
|
||||
if (isSearch) {
|
||||
postDraft = state.views.search;
|
||||
} else if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
const threadDraft = state.views.thread.drafts[rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
}
|
||||
@@ -29,16 +37,28 @@ function mapStateToProps(state, ownProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const value = postDraft.substring(0, cursorPosition);
|
||||
const matchTerm = getMatchTermForAtMention(value, isSearch);
|
||||
|
||||
let teamMembers;
|
||||
let inChannel;
|
||||
let outChannel;
|
||||
if (isSearch) {
|
||||
teamMembers = filterMembersInCurrentTeam(state, matchTerm);
|
||||
} else {
|
||||
inChannel = filterMembersInChannel(state, matchTerm);
|
||||
outChannel = filterMembersNotInChannel(state, matchTerm);
|
||||
}
|
||||
|
||||
return {
|
||||
currentUserId: state.entities.users.currentUserId,
|
||||
currentChannelId,
|
||||
currentTeamId: state.entities.teams.currentTeamId,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
defaultChannel: getDefaultChannel(state),
|
||||
postDraft,
|
||||
autocompleteUsers: {
|
||||
inChannel: getProfilesInCurrentChannel(state),
|
||||
outChannel: getProfilesNotInCurrentChannel(state)
|
||||
},
|
||||
matchTerm,
|
||||
teamMembers,
|
||||
inChannel,
|
||||
outChannel,
|
||||
requestStatus: state.requests.users.autocompleteUsers.status,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AtMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, username} = this.props;
|
||||
onPress(username);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
userId,
|
||||
username,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const hasFullName = firstName.length > 0 && lastName.length > 0;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={userId}
|
||||
onPress={this.completeMention}
|
||||
style={style.row}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
theme={theme}
|
||||
size={20}
|
||||
status={null}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${username}`}</Text>
|
||||
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
|
||||
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
width: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
rowUsername: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
rowFullname: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6
|
||||
}
|
||||
};
|
||||
});
|
||||
24
app/components/autocomplete/at_mention_item/index.js
Normal file
24
app/components/autocomplete/at_mention_item/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import AtMentionItem from './at_mention_item';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const user = getUser(state, ownProps.userId);
|
||||
|
||||
return {
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
username: user.username,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AtMentionItem);
|
||||
58
app/components/autocomplete/autocomplete_section_header.js
Normal file
58
app/components/autocomplete/autocomplete_section_header.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AutocompleteSectionHeader extends PureComponent {
|
||||
static propTypes = {
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {defaultMessage, id, theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.section}>
|
||||
<FormattedText
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
style={style.sectionText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
section: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
sectionText: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
paddingVertical: 7
|
||||
},
|
||||
sectionWrapper: {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,37 +1,34 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ListView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {SectionList} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
|
||||
const CHANNEL_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class ChannelMention extends Component {
|
||||
export default class ChannelMention extends PureComponent {
|
||||
static propTypes = {
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
autocompleteChannels: PropTypes.object.isRequired,
|
||||
postDraft: PropTypes.string,
|
||||
isSearch: PropTypes.bool,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
searchChannels: PropTypes.func.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
matchTerm: PropTypes.string,
|
||||
myChannels: PropTypes.array,
|
||||
otherChannels: PropTypes.array,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
postDraft: PropTypes.string,
|
||||
privateChannels: PropTypes.array,
|
||||
publicChannels: PropTypes.array,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -42,97 +39,82 @@ export default class ChannelMention extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const ds = new ListView.DataSource({
|
||||
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
|
||||
rowHasChanged: (r1, r2) => r1 !== r2
|
||||
});
|
||||
|
||||
this.state = {
|
||||
active: false,
|
||||
dataSource: ds.cloneWithRowsAndSections(props.autocompleteChannels)
|
||||
sections: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {isSearch} = nextProps;
|
||||
const regex = isSearch ? CHANNEL_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
|
||||
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, requestStatus} = nextProps;
|
||||
|
||||
// If not match or if user clicked on a channel
|
||||
if (!match || this.state.mentionComplete) {
|
||||
const nextState = {
|
||||
active: false,
|
||||
mentionComplete: false
|
||||
};
|
||||
|
||||
// Handle the case where the user typed a ~ first and then backspaced
|
||||
if (nextProps.postDraft.length < this.props.postDraft.length) {
|
||||
nextState.matchTerm = null;
|
||||
}
|
||||
|
||||
this.setState(nextState);
|
||||
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
|
||||
// if the term changes but is null or the mention has been completed we render this component as null
|
||||
this.setState({
|
||||
mentionComplete: false,
|
||||
sections: []
|
||||
});
|
||||
return;
|
||||
} else if (matchTerm === null) {
|
||||
// if the terms did not change but is null then we don't need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
const matchTerm = isSearch ? match[1] : match[2];
|
||||
const myChannels = this.filter(nextProps.autocompleteChannels.myChannels, matchTerm);
|
||||
const otherChannels = this.filter(nextProps.autocompleteChannels.otherChannels, matchTerm);
|
||||
|
||||
// Show loading indicator on first pull for channels
|
||||
if (nextProps.requestStatus === RequestStatus.STARTED && ((myChannels.length === 0 && otherChannels.length === 0) || matchTerm === '')) {
|
||||
this.setState({
|
||||
active: true,
|
||||
loading: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Still matching the same term that didn't return any results
|
||||
let startsWith;
|
||||
if (isSearch) {
|
||||
startsWith = match[0].startsWith(`in:${this.state.matchTerm}`) || match[0].startsWith(`channel:${this.state.matchTerm}`);
|
||||
} else {
|
||||
startsWith = match[0].startsWith(`~${this.state.matchTerm}`);
|
||||
}
|
||||
|
||||
if (startsWith && (myChannels.length === 0 && otherChannels.length === 0)) {
|
||||
this.setState({
|
||||
active: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchTerm !== this.state.matchTerm) {
|
||||
this.setState({
|
||||
matchTerm
|
||||
});
|
||||
|
||||
if (matchTerm !== this.props.matchTerm) {
|
||||
// if the term changed and we haven't made the request do that first
|
||||
const {currentTeamId} = this.props;
|
||||
this.props.actions.searchChannels(currentTeamId, matchTerm);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextProps.requestStatus !== RequestStatus.STARTED && this.props.autocompleteChannels !== nextProps.autocompleteChannels) {
|
||||
let data = {};
|
||||
if (myChannels.length > 0) {
|
||||
data = Object.assign({}, data, {myChannels});
|
||||
}
|
||||
if (otherChannels.length > 0) {
|
||||
data = Object.assign({}, data, {otherChannels});
|
||||
if (requestStatus !== RequestStatus.STARTED &&
|
||||
(myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
|
||||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels)) {
|
||||
// if the request is complete and the term is not null we show the autocomplete
|
||||
const sections = [];
|
||||
if (isSearch) {
|
||||
if (publicChannels.length) {
|
||||
sections.push({
|
||||
id: 'suggestion.search.public',
|
||||
defaultMessage: 'Public Channels',
|
||||
data: publicChannels,
|
||||
key: 'publicChannels'
|
||||
});
|
||||
}
|
||||
|
||||
if (privateChannels.length) {
|
||||
sections.push({
|
||||
id: 'suggestion.search.private',
|
||||
defaultMessage: 'Private Channels',
|
||||
data: privateChannels,
|
||||
key: 'privateChannels'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (myChannels.length) {
|
||||
sections.push({
|
||||
id: 'suggestion.mention.channels',
|
||||
defaultMessage: 'My Channels',
|
||||
data: myChannels,
|
||||
key: 'myChannels'
|
||||
});
|
||||
}
|
||||
|
||||
if (otherChannels.length) {
|
||||
sections.push({
|
||||
id: 'suggestion.mention.morechannels',
|
||||
defaultMessage: 'Other Channels',
|
||||
data: otherChannels,
|
||||
key: 'otherChannels'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active: true,
|
||||
loading: false,
|
||||
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
|
||||
sections
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
filter = (channels, matchTerm) => {
|
||||
return channels.filter((c) => c.name.includes(matchTerm) || c.display_name.includes(matchTerm));
|
||||
};
|
||||
|
||||
completeMention = (mention) => {
|
||||
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
|
||||
const mentionPart = postDraft.substring(0, cursorPosition);
|
||||
@@ -140,7 +122,7 @@ export default class ChannelMention extends Component {
|
||||
let completedDraft;
|
||||
if (isSearch) {
|
||||
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
|
||||
completedDraft = mentionPart.replace(CHANNEL_SEARCH_REGEX, `${channelOrIn} ${mention} `);
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `);
|
||||
} else {
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
|
||||
}
|
||||
@@ -149,88 +131,54 @@ export default class ChannelMention extends Component {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
this.setState({
|
||||
active: false,
|
||||
mentionComplete: true,
|
||||
matchTerm: `${mention} `
|
||||
});
|
||||
onChangeText(completedDraft, true);
|
||||
this.setState({mentionComplete: true});
|
||||
};
|
||||
|
||||
renderSectionHeader = (sectionData, sectionId) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
const localization = {
|
||||
myChannels: {
|
||||
id: 'suggestion.mention.channels',
|
||||
defaultMessage: 'My Channels'
|
||||
},
|
||||
otherChannels: {
|
||||
id: 'suggestion.mention.morechannels',
|
||||
defaultMessage: 'Other Channels'
|
||||
}
|
||||
};
|
||||
keyExtractor = (item) => {
|
||||
return item.id || item;
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.section}>
|
||||
<FormattedText
|
||||
id={localization[sectionId].id}
|
||||
defaultMessage={localization[sectionId].defaultMessage}
|
||||
style={style.sectionText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<AutocompleteSectionHeader
|
||||
id={section.id}
|
||||
defaultMessage={section.defaultMessage}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderRow = (data) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
renderItem = ({item}) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => this.completeMention(data.name)}
|
||||
style={style.row}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{data.display_name}</Text>
|
||||
<Text style={style.rowName}>{` (~${data.name})`}</Text>
|
||||
</TouchableOpacity>
|
||||
<ChannelMentionItem
|
||||
channelId={item}
|
||||
onPress={this.completeMention}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.active) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
const {isSearch, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
// If we are not in an active state or the mention has been completed return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
}
|
||||
|
||||
const {requestStatus, theme} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (this.state.loading && requestStatus === RequestStatus.STARTED) {
|
||||
return (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
style={style.sectionText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListView
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={style.listView}
|
||||
enableEmptySections={true}
|
||||
dataSource={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, isSearch ? style.search : null]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderRow={this.renderRow}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
initialNumToRender={10}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -238,57 +186,11 @@ export default class ChannelMention extends Component {
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
section: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
sectionText: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
paddingVertical: 7
|
||||
},
|
||||
sectionWrapper: {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
loading: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 20,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 0
|
||||
},
|
||||
row: {
|
||||
padding: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowDisplayName: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
rowName: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6
|
||||
search: {
|
||||
height: 250
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,21 +5,29 @@ import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {searchChannels} from 'mattermost-redux/actions/channels';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
filterMyChannels,
|
||||
filterOtherChannels,
|
||||
filterPublicChannels,
|
||||
filterPrivateChannels,
|
||||
getMatchTermForChannelMention
|
||||
} from 'app/selectors/autocomplete';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import ChannelMention from './channel_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
const {cursorPosition, isSearch, rootId} = ownProps;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
let postDraft;
|
||||
if (ownProps.isSearch) {
|
||||
let postDraft = '';
|
||||
if (isSearch) {
|
||||
postDraft = state.views.search;
|
||||
} else if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
} else if (rootId) {
|
||||
const threadDraft = state.views.thread.drafts[rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
}
|
||||
@@ -30,17 +38,30 @@ function mapStateToProps(state, ownProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const autocompleteChannels = {
|
||||
myChannels: getMyChannels(state).filter((c) => c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL),
|
||||
otherChannels: getOtherChannels(state)
|
||||
};
|
||||
const value = postDraft.substring(0, cursorPosition);
|
||||
const matchTerm = getMatchTermForChannelMention(value, isSearch);
|
||||
|
||||
let myChannels;
|
||||
let otherChannels;
|
||||
let publicChannels;
|
||||
let privateChannels;
|
||||
if (isSearch) {
|
||||
publicChannels = filterPublicChannels(state, matchTerm);
|
||||
privateChannels = filterPrivateChannels(state, matchTerm);
|
||||
} else {
|
||||
myChannels = filterMyChannels(state, matchTerm);
|
||||
otherChannels = filterOtherChannels(state, matchTerm);
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
currentChannelId,
|
||||
currentTeamId: state.entities.teams.currentTeamId,
|
||||
myChannels,
|
||||
otherChannels,
|
||||
publicChannels,
|
||||
privateChannels,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
matchTerm,
|
||||
postDraft,
|
||||
autocompleteChannels,
|
||||
requestStatus: state.requests.channels.getChannels.status,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class ChannelMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, name} = this.props;
|
||||
onPress(name);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelId,
|
||||
displayName,
|
||||
name,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={channelId}
|
||||
onPress={this.completeMention}
|
||||
style={style.row}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{displayName}</Text>
|
||||
<Text style={style.rowName}>{` (~${name})`}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
padding: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowDisplayName: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
rowName: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6
|
||||
}
|
||||
};
|
||||
});
|
||||
23
app/components/autocomplete/channel_mention_item/index.js
Normal file
23
app/components/autocomplete/channel_mention_item/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import ChannelMentionItem from './channel_mention_item';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const channel = getChannel(state, ownProps.channelId);
|
||||
|
||||
return {
|
||||
displayName: channel.display_name,
|
||||
name: channel.name,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelMentionItem);
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -12,27 +13,7 @@ import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: 200,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: 300,
|
||||
overflow: 'hidden',
|
||||
zIndex: 5
|
||||
}
|
||||
});
|
||||
|
||||
export default class Autocomplete extends Component {
|
||||
export default class Autocomplete extends PureComponent {
|
||||
static propTypes = {
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
@@ -54,9 +35,10 @@ export default class Autocomplete extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const container = this.props.isSearch ? style.searchContainer : style.container;
|
||||
const searchContainer = this.props.isSearch ? style.searchContainer : null;
|
||||
const container = this.props.isSearch ? null : style.container;
|
||||
return (
|
||||
<View>
|
||||
<View style={searchContainer}>
|
||||
<View style={container}>
|
||||
<AtMention
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
@@ -75,3 +57,32 @@ export default class Autocomplete extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: 200,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchContainer: {
|
||||
elevation: 5,
|
||||
flex: 1,
|
||||
left: 0,
|
||||
maxHeight: 250,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 5,
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 47
|
||||
},
|
||||
ios: {
|
||||
top: 64
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
106
app/components/autocomplete/special_mention_item.js
Normal file
106
app/components/autocomplete/special_mention_item.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class SpecialMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
completeHandle: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, completeHandle} = this.props;
|
||||
onPress(completeHandle);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultMessage,
|
||||
id,
|
||||
completeHandle,
|
||||
theme,
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.completeMention}
|
||||
style={style.row}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<Icon
|
||||
name='users'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.textWrapper}>
|
||||
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
|
||||
<Text style={style.rowUsername}>{' - '}</Text>
|
||||
<FormattedText
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
values={values}
|
||||
style={style.rowFullname}
|
||||
/>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
width: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
rowIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
fontSize: 14
|
||||
},
|
||||
rowUsername: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
rowFullname: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
opacity: 0.6
|
||||
},
|
||||
textWrapper: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
paddingRight: 8
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -74,6 +74,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -94,6 +95,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
handleAndroidBack = () => {
|
||||
@@ -106,7 +108,9 @@ export default class ChannelDrawer extends PureComponent {
|
||||
};
|
||||
|
||||
closeChannelDrawer = () => {
|
||||
this.setState({openDrawer: false});
|
||||
if (this.mounted) {
|
||||
this.setState({openDrawer: false});
|
||||
}
|
||||
};
|
||||
|
||||
drawerSwiperRef = (ref) => {
|
||||
@@ -121,7 +125,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
this.closeLeftHandle = null;
|
||||
}
|
||||
|
||||
if (this.state.openDrawer) {
|
||||
if (this.state.openDrawer && this.mounted) {
|
||||
// The state doesn't get updated if you swipe to close
|
||||
this.setState({
|
||||
openDrawer: false
|
||||
@@ -133,7 +137,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
if (!this.closeLeftHandle) {
|
||||
this.closeLeftHandle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
if (this.state.openDrawerOffset !== 0) {
|
||||
@@ -151,7 +155,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
this.openLeftHandle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
|
||||
if (!this.state.openDrawer) {
|
||||
if (!this.state.openDrawer && this.mounted) {
|
||||
// The state doesn't get updated if you swipe to open
|
||||
this.setState({
|
||||
openDrawer: true
|
||||
@@ -188,9 +192,11 @@ export default class ChannelDrawer extends PureComponent {
|
||||
openChannelDrawer = () => {
|
||||
this.props.blurPostTextBox();
|
||||
|
||||
this.setState({
|
||||
openDrawer: true
|
||||
});
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
openDrawer: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
selectChannel = (channel) => {
|
||||
@@ -207,18 +213,17 @@ export default class ChannelDrawer extends PureComponent {
|
||||
viewChannel
|
||||
} = actions;
|
||||
|
||||
markChannelAsRead(channel.id, currentChannelId);
|
||||
|
||||
if (channel.id !== currentChannelId) {
|
||||
setChannelLoading();
|
||||
viewChannel(currentChannelId);
|
||||
setChannelDisplayName(channel.display_name);
|
||||
}
|
||||
setChannelLoading();
|
||||
setChannelDisplayName(channel.display_name);
|
||||
|
||||
this.closeChannelDrawer();
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
handleSelectChannel(channel.id);
|
||||
markChannelAsRead(channel.id, currentChannelId);
|
||||
if (channel.id !== currentChannelId) {
|
||||
viewChannel(currentChannelId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -310,28 +315,37 @@ export default class ChannelDrawer extends PureComponent {
|
||||
openDrawerOffset
|
||||
} = this.state;
|
||||
|
||||
const showTeams = openDrawerOffset !== 0 && teamsCount > 1;
|
||||
const multipleTeams = teamsCount > 1;
|
||||
const showTeams = openDrawerOffset !== 0 && multipleTeams;
|
||||
if (this.drawerSwiper) {
|
||||
if (multipleTeams) {
|
||||
this.drawerSwiper.getWrappedInstance().runOnLayout();
|
||||
} else if (!openDrawerOffset) {
|
||||
this.drawerSwiper.getWrappedInstance().scrollToStart();
|
||||
}
|
||||
}
|
||||
|
||||
let teams;
|
||||
if (showTeams) {
|
||||
teams = (
|
||||
<View style={style.swiperContent}>
|
||||
const lists = [];
|
||||
if (multipleTeams) {
|
||||
const teamsList = (
|
||||
<View
|
||||
key='teamsList'
|
||||
style={style.swiperContent}
|
||||
>
|
||||
<TeamsList
|
||||
closeChannelDrawer={this.closeChannelDrawer}
|
||||
navigator={navigator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (this.drawerSwiper) {
|
||||
this.drawerSwiper.getWrappedInstance().runOnLayout();
|
||||
}
|
||||
} else if (this.drawerSwiper && !openDrawerOffset) {
|
||||
this.drawerSwiper.getWrappedInstance().scrollToStart();
|
||||
lists.push(teamsList);
|
||||
}
|
||||
|
||||
const channelsList = (
|
||||
<View style={style.swiperContent}>
|
||||
lists.push(
|
||||
<View
|
||||
key='channelsList'
|
||||
style={style.swiperContent}
|
||||
>
|
||||
<ChannelsList
|
||||
navigator={navigator}
|
||||
onSelectChannel={this.selectChannel}
|
||||
@@ -351,8 +365,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
showTeams={showTeams}
|
||||
theme={theme}
|
||||
>
|
||||
{teams}
|
||||
{channelsList}
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
);
|
||||
};
|
||||
@@ -368,6 +381,7 @@ export default class ChannelDrawer extends PureComponent {
|
||||
onOpenStart={this.handleDrawerOpenStart}
|
||||
onOpen={this.handleDrawerOpen}
|
||||
onClose={this.handleDrawerClose}
|
||||
onCloseStart={this.handleDrawerCloseStart}
|
||||
captureGestures='open'
|
||||
type='static'
|
||||
acceptTap={true}
|
||||
|
||||
@@ -26,9 +26,9 @@ export default class ChannelItem extends PureComponent {
|
||||
|
||||
onPress = () => {
|
||||
const {channel, onSelectChannel} = this.props;
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
preventDoubleTap(onSelectChannel, this, channel);
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class SwitchTeams extends React.PureComponent {
|
||||
static propTypes = {
|
||||
currentTeam: PropTypes.object.isRequired,
|
||||
currentTeam: PropTypes.object,
|
||||
searching: PropTypes.bool.isRequired,
|
||||
showTeams: PropTypes.func.isRequired,
|
||||
teamMembers: PropTypes.object.isRequired,
|
||||
@@ -78,11 +78,15 @@ export default class SwitchTeams extends React.PureComponent {
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
if (!currentTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
badgeCount
|
||||
} = this.state;
|
||||
|
||||
if (searching || teamMembers.length < 2) {
|
||||
if (searching || Object.keys(teamMembers).length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,12 @@ export default class DrawerSwiper extends PureComponent {
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const initialPage = React.Children.count(children) - 1;
|
||||
|
||||
return (
|
||||
<Swiper
|
||||
ref='swiper'
|
||||
initialPage={1}
|
||||
initialPage={initialPage}
|
||||
onIndexChanged={this.swiperPageSelected}
|
||||
paginationStyle={style.pagination}
|
||||
width={deviceWidth - openDrawerOffset}
|
||||
|
||||
@@ -4,38 +4,23 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId, getJoinableTeams, getMyTeams, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId, getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {handleTeamChange} from 'app/actions/views/select_team';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {getMySortedTeams} from 'app/selectors/teams';
|
||||
import {removeProtocol} from 'app/utils/url';
|
||||
|
||||
import TeamsList from './teams_list';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const user = getCurrentUser(state);
|
||||
|
||||
function sortTeams(locale, a, b) {
|
||||
if (a.display_name !== b.display_name) {
|
||||
return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true});
|
||||
}
|
||||
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
|
||||
}
|
||||
|
||||
return {
|
||||
canCreateTeams: false,
|
||||
joinableTeams: getJoinableTeams(state),
|
||||
currentChannelId: getCurrentChannelId(state),
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
teams: getMyTeams(state).sort(sortTeams.bind(null, (user.locale))),
|
||||
teams: getMySortedTeams(state),
|
||||
theme: getTheme(state),
|
||||
myTeamMembers: getTeamMemberships(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
@@ -43,8 +28,7 @@ function mapStateToProps(state, ownProps) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
handleTeamChange,
|
||||
markChannelAsRead
|
||||
handleTeamChange
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
InteractionManager,
|
||||
FlatList,
|
||||
Platform,
|
||||
Text,
|
||||
@@ -12,28 +11,24 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import Badge from 'app/components/badge';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import TeamsListItem from './teams_list_item';
|
||||
|
||||
class TeamsList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
handleTeamChange: PropTypes.func.isRequired,
|
||||
markChannelAsRead: PropTypes.func.isRequired
|
||||
handleTeamChange: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
canCreateTeams: PropTypes.bool.isRequired,
|
||||
closeChannelDrawer: PropTypes.func.isRequired,
|
||||
currentChannelId: PropTypes.string,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUrl: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
joinableTeams: PropTypes.object.isRequired,
|
||||
myTeamMembers: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
teams: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
@@ -48,21 +43,17 @@ class TeamsList extends PureComponent {
|
||||
}
|
||||
|
||||
selectTeam = (team) => {
|
||||
const {actions, closeChannelDrawer, currentChannelId, currentTeamId} = this.props;
|
||||
if (team.id === currentTeamId) {
|
||||
closeChannelDrawer();
|
||||
} else {
|
||||
actions.handleTeamChange(team);
|
||||
requestAnimationFrame(() => {
|
||||
const {actions, closeChannelDrawer, currentTeamId} = this.props;
|
||||
if (team.id !== currentTeamId) {
|
||||
actions.handleTeamChange(team);
|
||||
}
|
||||
|
||||
closeChannelDrawer();
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
actions.markChannelAsRead(currentChannelId);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
goToSelectTeam = () => {
|
||||
goToSelectTeam = wrapWithPreventDoubleTap(() => {
|
||||
const {currentUrl, intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
@@ -88,83 +79,18 @@ class TeamsList extends PureComponent {
|
||||
theme
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
keyExtractor = (team) => {
|
||||
return team.id;
|
||||
}
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {currentTeamId, currentUrl, myTeamMembers, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let current;
|
||||
let badge;
|
||||
if (item.id === currentTeamId) {
|
||||
current = (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<IonIcon
|
||||
name='md-checkmark'
|
||||
style={styles.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const member = myTeamMembers[item.id];
|
||||
|
||||
let badgeCount = 0;
|
||||
if (member.mention_count) {
|
||||
badgeCount = member.mention_count;
|
||||
} else if (member.msg_count) {
|
||||
badgeCount = -1;
|
||||
}
|
||||
|
||||
if (badgeCount) {
|
||||
badge = (
|
||||
<Badge
|
||||
style={styles.badge}
|
||||
countStyle={styles.mention}
|
||||
count={badgeCount}
|
||||
minHeight={20}
|
||||
minWidth={20}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.teamWrapper}>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={() => {
|
||||
setTimeout(() => {
|
||||
preventDoubleTap(this.selectTeam, this, item);
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
<View style={styles.teamContainer}>
|
||||
<View style={styles.teamIconContainer}>
|
||||
<Text style={styles.teamIcon}>
|
||||
{item.display_name.substr(0, 2).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.teamNameContainer}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.teamName}
|
||||
>
|
||||
{item.display_name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.teamUrl}
|
||||
>
|
||||
{`${currentUrl}/${item.name}`}
|
||||
</Text>
|
||||
</View>
|
||||
{current}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
{badge}
|
||||
</View>
|
||||
<TeamsListItem
|
||||
selectTeam={this.selectTeam}
|
||||
team={item}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -177,7 +103,7 @@ class TeamsList extends PureComponent {
|
||||
moreAction = (
|
||||
<TouchableHighlight
|
||||
style={styles.moreActionContainer}
|
||||
onPress={() => preventDoubleTap(this.goToSelectTeam)}
|
||||
onPress={this.goToSelectTeam}
|
||||
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
|
||||
>
|
||||
<Text
|
||||
@@ -204,7 +130,7 @@ class TeamsList extends PureComponent {
|
||||
<FlatList
|
||||
data={teams}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
keyExtractor={this.keyExtractor}
|
||||
viewabilityConfig={{
|
||||
viewAreaCoveragePercentThreshold: 3,
|
||||
waitForInteraction: false
|
||||
@@ -267,64 +193,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
moreAction: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 30
|
||||
},
|
||||
teamWrapper: {
|
||||
marginTop: 20
|
||||
},
|
||||
teamContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 16
|
||||
},
|
||||
teamIconContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.sidebarText,
|
||||
borderRadius: 2,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
width: 40
|
||||
},
|
||||
teamIcon: {
|
||||
color: theme.sidebarBg,
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 18,
|
||||
fontWeight: '600'
|
||||
},
|
||||
teamNameContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
marginLeft: 10
|
||||
},
|
||||
teamName: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 18
|
||||
},
|
||||
teamUrl: {
|
||||
color: changeOpacity(theme.sidebarText, 0.5),
|
||||
fontSize: 12
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
checkmark: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 20
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: theme.mentionBj,
|
||||
borderColor: theme.sidebarHeaderBg,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
padding: 3,
|
||||
position: 'absolute',
|
||||
left: 45,
|
||||
top: -7.5
|
||||
},
|
||||
mention: {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 10
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {removeProtocol} from 'app/utils/url';
|
||||
|
||||
import TeamsListItem from './teams_list_item.js';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
teamMember: getTeamMemberships(state)[ownProps.team.id],
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TeamsListItem);
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import Badge from 'app/components/badge';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class TeamsListItem extends React.PureComponent {
|
||||
static propTypes = {
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUrl: PropTypes.string.isRequired,
|
||||
selectTeam: PropTypes.func.isRequired,
|
||||
team: PropTypes.object.isRequired,
|
||||
teamMember: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
selectTeam = wrapWithPreventDoubleTap(() => {
|
||||
this.props.selectTeam(this.props.team);
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentTeamId,
|
||||
currentUrl,
|
||||
team,
|
||||
teamMember,
|
||||
theme
|
||||
} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let current;
|
||||
let badge;
|
||||
if (team.id === currentTeamId) {
|
||||
current = (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<IonIcon
|
||||
name='md-checkmark'
|
||||
style={styles.checkmark}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let badgeCount = 0;
|
||||
if (teamMember.mention_count) {
|
||||
badgeCount = teamMember.mention_count;
|
||||
} else if (teamMember.msg_count) {
|
||||
badgeCount = -1;
|
||||
}
|
||||
|
||||
if (badgeCount) {
|
||||
badge = (
|
||||
<Badge
|
||||
style={styles.badge}
|
||||
countStyle={styles.mention}
|
||||
count={badgeCount}
|
||||
minHeight={20}
|
||||
minWidth={20}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.teamWrapper}>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.selectTeam}
|
||||
>
|
||||
<View style={styles.teamContainer}>
|
||||
<View style={styles.teamIconContainer}>
|
||||
<Text style={styles.teamIcon}>
|
||||
{team.display_name.substr(0, 2).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.teamNameContainer}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.teamName}
|
||||
>
|
||||
{team.display_name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.teamUrl}
|
||||
>
|
||||
{`${currentUrl}/${team.name}`}
|
||||
</Text>
|
||||
</View>
|
||||
{current}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
{badge}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
teamWrapper: {
|
||||
marginTop: 20
|
||||
},
|
||||
teamContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 16
|
||||
},
|
||||
teamIconContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.sidebarText,
|
||||
borderRadius: 2,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
width: 40
|
||||
},
|
||||
teamIcon: {
|
||||
color: theme.sidebarBg,
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 18,
|
||||
fontWeight: '600'
|
||||
},
|
||||
teamNameContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
marginLeft: 10
|
||||
},
|
||||
teamName: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 18
|
||||
},
|
||||
teamUrl: {
|
||||
color: changeOpacity(theme.sidebarText, 0.5),
|
||||
fontSize: 12
|
||||
},
|
||||
checkmarkContainer: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
checkmark: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 20
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: theme.mentionBj,
|
||||
borderColor: theme.sidebarHeaderBg,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
padding: 3,
|
||||
position: 'absolute',
|
||||
left: 45,
|
||||
top: -7.5
|
||||
},
|
||||
mention: {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 10
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -71,7 +71,7 @@ class ChannelIntro extends PureComponent {
|
||||
style={style.profile}
|
||||
>
|
||||
<ProfilePicture
|
||||
user={member}
|
||||
userId={member.id}
|
||||
size={64}
|
||||
statusBorderWidth={2}
|
||||
statusSize={25}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const GRADIENT_START = 0.05;
|
||||
const GRADIENT_MIDDLE = 0.1;
|
||||
const GRADIENT_END = 0.01;
|
||||
|
||||
function buildSections(key, style, theme, top) {
|
||||
return (
|
||||
<View
|
||||
key={key}
|
||||
style={[style.section, (top && {marginTop: -15})]}
|
||||
>
|
||||
<View style={style.avatar}/>
|
||||
<View style={style.sectionMessage}>
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 1.0}}
|
||||
end={{x: 1.0, y: 1.0}}
|
||||
colors={[
|
||||
changeOpacity('#e5e5e4', GRADIENT_START),
|
||||
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
|
||||
changeOpacity('#e5e5e4', GRADIENT_END)
|
||||
]}
|
||||
locations={[0.1, 0.3, 0.7]}
|
||||
style={[style.messageText, {width: 106}]}
|
||||
/>
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 1.0}}
|
||||
end={{x: 1.0, y: 1.0}}
|
||||
colors={[
|
||||
changeOpacity('#e5e5e4', GRADIENT_START),
|
||||
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
|
||||
changeOpacity('#e5e5e4', GRADIENT_END)
|
||||
]}
|
||||
locations={[0.1, 0.3, 0.7]}
|
||||
style={[style.messageText, {alignSelf: 'stretch'}]}
|
||||
/>
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 1.0}}
|
||||
end={{x: 1.0, y: 1.0}}
|
||||
colors={[
|
||||
changeOpacity('#e5e5e4', GRADIENT_START),
|
||||
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
|
||||
changeOpacity('#e5e5e4', GRADIENT_END)
|
||||
]}
|
||||
locations={[0.1, 0.3, 0.7]}
|
||||
style={[style.messageText, {alignSelf: 'stretch'}]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function channelLoader(props) {
|
||||
const style = getStyleSheet(props.theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{Array(10).fill().map((item, index) => buildSections(index, style, props.theme, index === 0))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
channelLoader.propTypes = {
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
avatar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderRadius: 16,
|
||||
height: 32,
|
||||
width: 32
|
||||
},
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1
|
||||
},
|
||||
messageText: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 10,
|
||||
marginBottom: 10
|
||||
},
|
||||
section: {
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 12,
|
||||
paddingRight: 20,
|
||||
marginVertical: 10
|
||||
},
|
||||
sectionMessage: {
|
||||
marginLeft: 12,
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
127
app/components/channel_loader/channel_loader.js
Normal file
127
app/components/channel_loader/channel_loader.js
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
View
|
||||
} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const GRADIENT_START = 0.05;
|
||||
const GRADIENT_MIDDLE = 0.1;
|
||||
const GRADIENT_END = 0.01;
|
||||
|
||||
export default class ChannelLoader extends PureComponent {
|
||||
static propTypes = {
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
buildSections(key, style, top) {
|
||||
return (
|
||||
<View
|
||||
key={key}
|
||||
style={[style.section, (top && {marginTop: Platform.OS === 'android' ? 0 : -15, paddingTop: 10})]}
|
||||
>
|
||||
<View style={style.avatar}/>
|
||||
<View style={style.sectionMessage}>
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 1.0}}
|
||||
end={{x: 1.0, y: 1.0}}
|
||||
colors={[
|
||||
changeOpacity('#e5e5e4', GRADIENT_START),
|
||||
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
|
||||
changeOpacity('#e5e5e4', GRADIENT_END)
|
||||
]}
|
||||
locations={[0.1, 0.3, 0.7]}
|
||||
style={[style.messageText, {width: 106}]}
|
||||
/>
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 1.0}}
|
||||
end={{x: 1.0, y: 1.0}}
|
||||
colors={[
|
||||
changeOpacity('#e5e5e4', GRADIENT_START),
|
||||
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
|
||||
changeOpacity('#e5e5e4', GRADIENT_END)
|
||||
]}
|
||||
locations={[0.1, 0.3, 0.7]}
|
||||
style={[style.messageText, {alignSelf: 'stretch'}]}
|
||||
/>
|
||||
<LinearGradient
|
||||
start={{x: 0.0, y: 1.0}}
|
||||
end={{x: 1.0, y: 1.0}}
|
||||
colors={[
|
||||
changeOpacity('#e5e5e4', GRADIENT_START),
|
||||
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
|
||||
changeOpacity('#e5e5e4', GRADIENT_END)
|
||||
]}
|
||||
locations={[0.1, 0.3, 0.7]}
|
||||
style={[style.messageText, {alignSelf: 'stretch'}]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {channelIsLoading, deviceWidth, theme} = this.props;
|
||||
|
||||
if (!channelIsLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={[style.container, {width: deviceWidth}]}>
|
||||
{Array(20).fill().map((item, index) => this.buildSections(index, style, index === 0))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 0
|
||||
},
|
||||
ios: {
|
||||
top: 15
|
||||
}
|
||||
})
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderRadius: 16,
|
||||
height: 32,
|
||||
width: 32
|
||||
},
|
||||
messageText: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 10,
|
||||
marginBottom: 10
|
||||
},
|
||||
section: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 12,
|
||||
paddingRight: 20,
|
||||
marginVertical: 10
|
||||
},
|
||||
sectionMessage: {
|
||||
marginLeft: 12,
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
19
app/components/channel_loader/index.js
Normal file
19
app/components/channel_loader/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {deviceWidth} = state.device.dimension;
|
||||
return {
|
||||
...ownProps,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
deviceWidth,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelLoader);
|
||||
@@ -40,7 +40,7 @@ export default class UserListRow extends React.PureComponent {
|
||||
selected={this.props.selected}
|
||||
>
|
||||
<ProfilePicture
|
||||
user={this.props.user}
|
||||
userId={this.props.user.id}
|
||||
size={32}
|
||||
/>
|
||||
<View style={style.textContainer}>
|
||||
|
||||
@@ -39,11 +39,11 @@ export default class OptionsContext extends PureComponent {
|
||||
};
|
||||
|
||||
handleHideUnderlay = () => {
|
||||
this.props.toggleSelected(false);
|
||||
this.props.toggleSelected(false, this.props.actions.length > 0);
|
||||
};
|
||||
|
||||
handleShowUnderlay = () => {
|
||||
this.props.toggleSelected(true);
|
||||
this.props.toggleSelected(true, this.props.actions.length > 0);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -17,12 +17,24 @@ export default class OptionsContext extends PureComponent {
|
||||
actions: []
|
||||
};
|
||||
|
||||
handleHideUnderlay = () => {
|
||||
if (!this.isShowing) {
|
||||
this.props.toggleSelected(false, false);
|
||||
}
|
||||
};
|
||||
|
||||
handleShowUnderlay = () => {
|
||||
this.props.toggleSelected(true, false);
|
||||
};
|
||||
|
||||
handleHide = () => {
|
||||
this.props.toggleSelected(false);
|
||||
this.isShowing = false;
|
||||
this.props.toggleSelected(false, this.props.actions.length > 0);
|
||||
};
|
||||
|
||||
handleShow = () => {
|
||||
this.props.toggleSelected(true);
|
||||
this.isShowing = this.props.actions.length > 0;
|
||||
this.props.toggleSelected(true, this.isShowing);
|
||||
};
|
||||
|
||||
hide = () => {
|
||||
@@ -33,14 +45,21 @@ export default class OptionsContext extends PureComponent {
|
||||
this.refs.toolTip.showMenu();
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
this.props.toggleSelected(false, this.props.actions.length > 0);
|
||||
this.props.onPress();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ToolTip
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
ref='toolTip'
|
||||
actions={this.props.actions}
|
||||
arrowDirection='down'
|
||||
longPress={true}
|
||||
onPress={this.props.onPress}
|
||||
onPress={this.handlePress}
|
||||
underlayColor='transparent'
|
||||
onShow={this.handleShow}
|
||||
onHide={this.handleHide}
|
||||
|
||||
@@ -281,15 +281,17 @@ class Post extends PureComponent {
|
||||
};
|
||||
|
||||
viewUserProfile = () => {
|
||||
const {isSearchResult} = this.props;
|
||||
const {isSearchResult, tooltipVisible} = this.props;
|
||||
|
||||
if (!isSearchResult) {
|
||||
if (!isSearchResult && !tooltipVisible) {
|
||||
preventDoubleTap(this.goToUserProfile, this);
|
||||
}
|
||||
};
|
||||
|
||||
toggleSelected = (selected) => {
|
||||
this.props.actions.setPostTooltipVisible(selected);
|
||||
toggleSelected = (selected, tooltip) => {
|
||||
if (tooltip) {
|
||||
this.props.actions.setPostTooltipVisible(selected);
|
||||
}
|
||||
this.setState({selected});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {isPostFlagged, isPostEphemeral, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
@@ -27,6 +27,7 @@ function mapStateToProps(state, ownProps) {
|
||||
isFailed: post.failed,
|
||||
isFlagged: isPostFlagged(post.id, myPreferences),
|
||||
isPending: post.id === post.pending_post_id,
|
||||
isPostEphemeral: isPostEphemeral(post),
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
message: post.message,
|
||||
theme: getTheme(state)
|
||||
|
||||
@@ -21,7 +21,6 @@ import PostBodyAdditionalContent from 'app/components/post_body_additional_conte
|
||||
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
||||
import {extractFirstLink} from 'app/utils/url';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Reactions from 'app/components/reactions';
|
||||
|
||||
@@ -40,6 +39,7 @@ class PostBody extends PureComponent {
|
||||
isFailed: PropTypes.bool,
|
||||
isFlagged: PropTypes.bool,
|
||||
isPending: PropTypes.bool,
|
||||
isPostEphemeral: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
message: PropTypes.string,
|
||||
@@ -67,20 +67,6 @@ class PostBody extends PureComponent {
|
||||
toggleSelected: emptyFunction
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
link: extractFirstLink(props.message)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.message !== this.props.message) {
|
||||
this.setState({link: extractFirstLink(nextProps.message)});
|
||||
}
|
||||
}
|
||||
|
||||
handleHideUnderlay = () => {
|
||||
this.props.toggleSelected(false);
|
||||
};
|
||||
@@ -148,6 +134,7 @@ class PostBody extends PureComponent {
|
||||
isFailed,
|
||||
isFlagged,
|
||||
isPending,
|
||||
isPostEphemeral,
|
||||
isSearchResult,
|
||||
isSystemMessage,
|
||||
intl,
|
||||
@@ -172,7 +159,7 @@ class PostBody extends PureComponent {
|
||||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPendingOrFailedPost && !isSearchResult) {
|
||||
if (!isPendingOrFailedPost && !isSearchResult && !isSystemMessage && !isPostEphemeral) {
|
||||
if (isFlagged) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}),
|
||||
@@ -234,24 +221,20 @@ class PostBody extends PureComponent {
|
||||
body = (
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onLongPress={this.show}
|
||||
onPress={onPress}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
>
|
||||
<View>
|
||||
{messageComponent}
|
||||
{Boolean(this.state.link) &&
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
link={this.state.link}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
/>
|
||||
}
|
||||
{this.renderFileAttachments()}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
@@ -266,17 +249,14 @@ class PostBody extends PureComponent {
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
{messageComponent}
|
||||
{Boolean(this.state.link) &&
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
link={this.state.link}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
/>
|
||||
}
|
||||
{this.renderFileAttachments()}
|
||||
{hasReactions && <Reactions postId={postId}/>}
|
||||
</OptionsContext>
|
||||
|
||||
@@ -14,21 +14,42 @@ import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {extractFirstLink} from 'app/utils/url';
|
||||
|
||||
import PostBodyAdditionalContent from './post_body_additional_content';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const config = getConfig(state);
|
||||
const previewsEnabled = getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, `${ViewTypes.FEATURE_TOGGLE_PREFIX}${ViewTypes.EMBED_PREVIEW}`);
|
||||
function makeGetFirstLink() {
|
||||
let link;
|
||||
let lastMessage;
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...getDimensions(state),
|
||||
config,
|
||||
openGraphData: getOpenGraphMetadataForUrl(state, ownProps.link),
|
||||
showLinkPreviews: previewsEnabled && config.EnableLinkPreviews === 'true',
|
||||
theme: getTheme(state)
|
||||
return (message) => {
|
||||
if (message !== lastMessage) {
|
||||
link = extractFirstLink(message);
|
||||
lastMessage = message;
|
||||
}
|
||||
|
||||
return link;
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PostBodyAdditionalContent);
|
||||
function makeMapStateToProps() {
|
||||
const getFirstLink = makeGetFirstLink();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const config = getConfig(state);
|
||||
const previewsEnabled = getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, `${ViewTypes.FEATURE_TOGGLE_PREFIX}${ViewTypes.EMBED_PREVIEW}`);
|
||||
const link = getFirstLink(ownProps.message);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...getDimensions(state),
|
||||
config,
|
||||
link,
|
||||
openGraphData: getOpenGraphMetadataForUrl(state, link),
|
||||
showLinkPreviews: previewsEnabled && config.EnableLinkPreviews === 'true',
|
||||
theme: getTheme(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(PostBodyAdditionalContent);
|
||||
|
||||
@@ -120,23 +120,25 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
const {link} = this.props;
|
||||
const {linkLoaded} = this.state;
|
||||
|
||||
let imageUrl;
|
||||
if (isImageLink(link)) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
if (link) {
|
||||
let imageUrl;
|
||||
if (isImageLink(link)) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
|
||||
if (imageUrl && !linkLoaded) {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
if (imageUrl && !linkLoaded) {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = this.calculateDimensions(width, height);
|
||||
this.setState({...dimensions, linkLoaded: true});
|
||||
}, () => null);
|
||||
const dimensions = this.calculateDimensions(width, height);
|
||||
this.setState({...dimensions, linkLoaded: true});
|
||||
}, () => null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,14 +246,20 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
render() {
|
||||
const {link, openGraphData} = this.props;
|
||||
const {linkLoaded, linkLoadError} = this.state;
|
||||
const isYouTube = isYoutubeLink(link);
|
||||
const isImage = isImageLink(link);
|
||||
const isOpenGraph = Boolean(openGraphData && openGraphData.description);
|
||||
let isYouTube = false;
|
||||
let isImage = false;
|
||||
let isOpenGraph = false;
|
||||
|
||||
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
|
||||
const embed = this.generateToggleableEmbed(isImage, isYouTube);
|
||||
if (embed && (linkLoaded || isYouTube)) {
|
||||
return embed;
|
||||
if (link) {
|
||||
isYouTube = isYoutubeLink(link);
|
||||
isImage = isImageLink(link);
|
||||
isOpenGraph = Boolean(openGraphData && openGraphData.description);
|
||||
|
||||
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
|
||||
const embed = this.generateToggleableEmbed(isImage, isYouTube);
|
||||
if (embed && (linkLoaded || isYouTube)) {
|
||||
return embed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getBool, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
@@ -21,6 +22,7 @@ function makeMapStateToProps() {
|
||||
const commentedOnUser = getUser(state, ownProps.commentedOnUserId);
|
||||
const user = getUser(state, post.user_id);
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
const militaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
@@ -30,6 +32,7 @@ function makeMapStateToProps() {
|
||||
displayName: displayUsername(user, teammateNameDisplay),
|
||||
enablePostUsernameOverride: config.EnablePostUsernameOverride === 'true',
|
||||
fromWebHook: post.props && post.props.from_webhook === 'true',
|
||||
militaryTime,
|
||||
isPendingOrFailedPost: isPostPendingOrFailed(post),
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
overrideUsername: post.props && post.props.override_username,
|
||||
|
||||
@@ -28,12 +28,13 @@ export default class PostHeader extends PureComponent {
|
||||
fromWebHook: PropTypes.bool,
|
||||
isPendingOrFailedPost: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
militaryTime: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
onViewUserProfile: PropTypes.func,
|
||||
overrideUsername: PropTypes.string,
|
||||
renderReplies: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
@@ -142,6 +143,7 @@ export default class PostHeader extends PureComponent {
|
||||
createAt,
|
||||
isPendingOrFailedPost,
|
||||
isSearchResult,
|
||||
militaryTime,
|
||||
onPress,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
@@ -159,14 +161,20 @@ export default class PostHeader extends PureComponent {
|
||||
<FormattedDate value={createAt}/>
|
||||
</Text>
|
||||
<Text style={style.time}>
|
||||
<FormattedTime value={createAt}/>
|
||||
<FormattedTime
|
||||
hour12={!militaryTime}
|
||||
value={createAt}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
dateComponent = (
|
||||
<Text style={style.time}>
|
||||
<FormattedTime value={createAt}/>
|
||||
<FormattedTime
|
||||
hour12={!militaryTime}
|
||||
value={createAt}
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ export default class PostList extends PureComponent {
|
||||
actions: PropTypes.shape({
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channel: PropTypes.object,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
channelId: PropTypes.string,
|
||||
currentUserId: PropTypes.string,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
@@ -44,11 +43,6 @@ export default class PostList extends PureComponent {
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
channel: {},
|
||||
channelIsLoading: false
|
||||
};
|
||||
|
||||
getPostsWithDates = () => {
|
||||
const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props;
|
||||
const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
|
||||
@@ -81,12 +75,12 @@ export default class PostList extends PureComponent {
|
||||
onRefresh = () => {
|
||||
const {
|
||||
actions,
|
||||
channel,
|
||||
channelId,
|
||||
onRefresh
|
||||
} = this.props;
|
||||
|
||||
if (Object.keys(channel).length) {
|
||||
actions.refreshChannelWithRetry(channel.id);
|
||||
if (channelId) {
|
||||
actions.refreshChannelWithRetry(channelId);
|
||||
}
|
||||
|
||||
if (onRefresh) {
|
||||
@@ -95,9 +89,9 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
renderChannelIntro = () => {
|
||||
const {channel, channelIsLoading, navigator, refreshing, showLoadMore} = this.props;
|
||||
const {channelId, navigator, refreshing, showLoadMore} = this.props;
|
||||
|
||||
if (channel.hasOwnProperty('id') && !showLoadMore && !refreshing && !channelIsLoading) {
|
||||
if (channelId && !showLoadMore && !refreshing) {
|
||||
return (
|
||||
<View>
|
||||
<ChannelIntro navigator={navigator}/>
|
||||
@@ -169,13 +163,13 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {channel, refreshing, theme} = this.props;
|
||||
const {channelId, refreshing, theme} = this.props;
|
||||
|
||||
const refreshControl = {
|
||||
refreshing
|
||||
};
|
||||
|
||||
if (Object.keys(channel).length) {
|
||||
if (channelId) {
|
||||
refreshControl.onRefresh = this.onRefresh;
|
||||
}
|
||||
|
||||
@@ -187,7 +181,7 @@ export default class PostList extends PureComponent {
|
||||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.renderChannelIntro}
|
||||
onEndReached={this.loadMorePosts}
|
||||
onEndReachedThreshold={700}
|
||||
onEndReachedThreshold={0}
|
||||
{...refreshControl}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
@@ -14,7 +13,6 @@ import PostProfilePicture from './post_profile_picture';
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {config} = state.entities.general;
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const user = getUser(state, post.user_id);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
@@ -22,7 +20,7 @@ function mapStateToProps(state, ownProps) {
|
||||
fromWebHook: post.props && post.props.from_webhook === 'true',
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
overrideIconUrl: post.props && post.props.override_icon_url,
|
||||
user,
|
||||
userId: post.user_id,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ function PostProfilePicture(props) {
|
||||
onViewUserProfile,
|
||||
overrideIconUrl,
|
||||
theme,
|
||||
user
|
||||
userId
|
||||
} = props;
|
||||
if (isSystemMessage) {
|
||||
return (
|
||||
@@ -51,7 +51,7 @@ function PostProfilePicture(props) {
|
||||
|
||||
return (
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
userId={userId}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
);
|
||||
@@ -60,7 +60,7 @@ function PostProfilePicture(props) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onViewUserProfile}>
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
userId={userId}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
@@ -74,7 +74,7 @@ PostProfilePicture.propTypes = {
|
||||
overrideIconUrl: PropTypes.string,
|
||||
onViewUserProfile: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
user: PropTypes.object
|
||||
userId: PropTypes.string
|
||||
};
|
||||
|
||||
PostProfilePicture.defaultProps = {
|
||||
|
||||
@@ -6,19 +6,21 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {getStatusesByIdsBatchedDebounced} from 'mattermost-redux/actions/users';
|
||||
import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import ProfilePicture from './profile_picture';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
let status = ownProps.status;
|
||||
if (!status && ownProps.user) {
|
||||
status = ownProps.user.status || getStatusForUserId(state, ownProps.user.id);
|
||||
const user = getUser(state, ownProps.userId);
|
||||
if (!status && ownProps.userId) {
|
||||
status = getStatusForUserId(state, ownProps.userId);
|
||||
}
|
||||
|
||||
return {
|
||||
theme: ownProps.theme || getTheme(state),
|
||||
status,
|
||||
user,
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export default class SearchBarIos extends PureComponent {
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
this.props.onChangeText('');
|
||||
this.props.onChangeText('', true);
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
|
||||
@@ -70,7 +70,9 @@ export default class Swiper extends PureComponent {
|
||||
onLayout = () => {
|
||||
if (this.runOnLayout) {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.scrollView.scrollTo({x: this.offset, animated: false});
|
||||
setTimeout(() => {
|
||||
this.scrollView.scrollTo({x: this.props.width * this.state.index, animated: false});
|
||||
}, 100);
|
||||
} else {
|
||||
this.scrollView.setPageWithoutAnimation(this.state.index);
|
||||
}
|
||||
|
||||
10
app/constants/autocomplete.js
Normal file
10
app/constants/autocomplete.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i;
|
||||
|
||||
export const AT_MENTION_SEARCH_REGEX = /\bfrom:\s*(\S*)$/i;
|
||||
|
||||
export const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
|
||||
|
||||
export const CHANNEL_MENTION_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;
|
||||
@@ -46,7 +46,9 @@ const ViewTypes = keyMirror({
|
||||
|
||||
INCREASE_POST_VISIBILITY: null,
|
||||
RECEIVED_FOCUSED_POST: null,
|
||||
LOADING_POSTS: null
|
||||
LOADING_POSTS: null,
|
||||
|
||||
RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: null
|
||||
});
|
||||
|
||||
export default {
|
||||
|
||||
@@ -8,11 +8,13 @@ import dimension from './dimension';
|
||||
import isTablet from './is_tablet';
|
||||
import orientation from './orientation';
|
||||
import statusBarHeight from './status_bar';
|
||||
import websocket from './websocket';
|
||||
|
||||
export default combineReducers({
|
||||
connection,
|
||||
dimension,
|
||||
isTablet,
|
||||
orientation,
|
||||
statusBarHeight
|
||||
statusBarHeight,
|
||||
websocket
|
||||
});
|
||||
|
||||
34
app/reducers/device/websocket.js
Normal file
34
app/reducers/device/websocket.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {GeneralTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
function getInitialState() {
|
||||
return {
|
||||
connected: false,
|
||||
lastConnectAt: 0,
|
||||
lastDisconnectAt: 0
|
||||
};
|
||||
}
|
||||
|
||||
export default function(state = getInitialState(), action) {
|
||||
if (!state.connected && action.type === GeneralTypes.WEBSOCKET_SUCCESS) {
|
||||
return {
|
||||
...state,
|
||||
connected: true,
|
||||
lastConnectAt: new Date().getTime()
|
||||
};
|
||||
} else if (state.connected && (action.type === GeneralTypes.WEBSOCKET_FAILURE || action.type === GeneralTypes.WEBSOCKET_CLOSED)) {
|
||||
return {
|
||||
...state,
|
||||
connected: false,
|
||||
lastDisconnectAt: new Date().getTime()
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === UserTypes.LOGOUT_SUCCESS) {
|
||||
return getInitialState();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -176,8 +176,6 @@ function drafts(state = {}, action) {
|
||||
|
||||
function loading(state = false, action) {
|
||||
switch (action.type) {
|
||||
case ChannelTypes.SELECT_CHANNEL:
|
||||
return false;
|
||||
case ViewTypes.SET_CHANNEL_LOADER:
|
||||
return action.loading;
|
||||
default:
|
||||
@@ -248,6 +246,19 @@ function loadingPosts(state = {}, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function lastGetPosts(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME:
|
||||
return {
|
||||
...state,
|
||||
[action.channelId]: action.time
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
displayName,
|
||||
drafts,
|
||||
@@ -255,5 +266,6 @@ export default combineReducers({
|
||||
refreshing,
|
||||
tooltipVisible,
|
||||
postVisibility,
|
||||
loadingPosts
|
||||
loadingPosts,
|
||||
lastGetPosts
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import ChannelDrawer from 'app/components/channel_drawer';
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
import Loading from 'app/components/loading';
|
||||
import OfflineIndicator from 'app/components/offline_indicator';
|
||||
@@ -34,12 +35,14 @@ class Channel extends PureComponent {
|
||||
connection: PropTypes.func.isRequired,
|
||||
loadChannelsIfNecessary: PropTypes.func.isRequired,
|
||||
loadProfilesAndTeamMembersForDMSidebar: PropTypes.func.isRequired,
|
||||
markChannelAsRead: PropTypes.func.isRequired,
|
||||
selectFirstAvailableTeam: PropTypes.func.isRequired,
|
||||
selectInitialChannel: PropTypes.func.isRequired,
|
||||
initWebSocket: PropTypes.func.isRequired,
|
||||
closeWebSocket: PropTypes.func.isRequired,
|
||||
startPeriodicStatusUpdates: PropTypes.func.isRequired,
|
||||
stopPeriodicStatusUpdates: PropTypes.func.isRequired
|
||||
stopPeriodicStatusUpdates: PropTypes.func.isRequired,
|
||||
viewChannel: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
@@ -69,6 +72,12 @@ class Channel extends PureComponent {
|
||||
} catch (error) {
|
||||
// We don't care about the error
|
||||
}
|
||||
|
||||
// Mark current channel as read when opening app while logged in
|
||||
if (this.props.currentChannelId) {
|
||||
this.props.actions.markChannelAsRead(this.props.currentChannelId);
|
||||
this.props.actions.viewChannel(this.props.currentChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -219,6 +228,7 @@ class Channel extends PureComponent {
|
||||
navigator={navigator}
|
||||
/>
|
||||
</View>
|
||||
<ChannelLoader theme={theme}/>
|
||||
<PostTextbox
|
||||
ref={this.attachPostTextbox}
|
||||
onChangeText={this.handleDraftChanged}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostListRetry from 'app/components/post_list_retry';
|
||||
@@ -29,22 +28,26 @@ class ChannelPostList extends PureComponent {
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channel: PropTypes.object.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
channelDisplayName: PropTypes.string,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
channelIsRefreshing: PropTypes.bool,
|
||||
channelRefreshingFailed: PropTypes.bool,
|
||||
channelType: PropTypes.string,
|
||||
currentUserId: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
lastViewedAt: PropTypes.number,
|
||||
loadingPosts: PropTypes.bool,
|
||||
myMember: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
posts: PropTypes.array.isRequired,
|
||||
postVisibility: PropTypes.number,
|
||||
totalMessageCount: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
posts: [],
|
||||
loadingPosts: false,
|
||||
postVisibility: 0
|
||||
postVisibility: 30
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -53,44 +56,42 @@ class ChannelPostList extends PureComponent {
|
||||
this.state = {
|
||||
retryMessageHeight: new Animated.Value(0),
|
||||
visiblePosts: this.getVisiblePosts(props),
|
||||
showLoadMore: false
|
||||
showLoadMore: props.posts.length >= props.postVisibility
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {channel, posts, channelRefreshingFailed} = this.props;
|
||||
const {channelId} = this.props;
|
||||
this.mounted = true;
|
||||
this.loadPosts(this.props.channel.id);
|
||||
this.shouldMarkChannelAsLoaded(posts.length, channel.total_msg_count === 0, channelRefreshingFailed);
|
||||
this.loadPosts(channelId);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {channel: currentChannel} = this.props;
|
||||
const {channel: nextChannel, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps;
|
||||
const {channelId: currentChannelId} = this.props;
|
||||
const {channelId: nextChannelId, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps;
|
||||
|
||||
if (currentChannel.id !== nextChannel.id) {
|
||||
if (currentChannelId !== nextChannelId) {
|
||||
// Load the posts when the channel actually changes
|
||||
this.loadPosts(nextChannel.id);
|
||||
this.loadPosts(nextChannelId);
|
||||
}
|
||||
|
||||
if (nextChannelRefreshingFailed && this.state.channelLoaded && nextPosts.length) {
|
||||
if (nextChannelRefreshingFailed && nextPosts.length) {
|
||||
this.toggleRetryMessage();
|
||||
} else if (!nextChannelRefreshingFailed || !nextPosts.length) {
|
||||
this.toggleRetryMessage(false);
|
||||
}
|
||||
|
||||
this.shouldMarkChannelAsLoaded(nextPosts.length, nextChannel.total_msg_count === 0, nextChannelRefreshingFailed);
|
||||
|
||||
const showLoadMore = nextProps.posts.length >= nextProps.postVisibility;
|
||||
this.setState({
|
||||
showLoadMore
|
||||
});
|
||||
let visiblePosts = this.state.visiblePosts;
|
||||
|
||||
if (nextProps.posts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) {
|
||||
this.setState({
|
||||
visiblePosts: this.getVisiblePosts(nextProps)
|
||||
});
|
||||
visiblePosts = this.getVisiblePosts(nextProps);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showLoadMore,
|
||||
visiblePosts
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -98,17 +99,7 @@ class ChannelPostList extends PureComponent {
|
||||
}
|
||||
|
||||
getVisiblePosts = (props) => {
|
||||
return props.posts.slice(0, props.posts.postVisibility);
|
||||
}
|
||||
|
||||
shouldMarkChannelAsLoaded = (postsCount, channelHasMessages, channelRefreshingFailed) => {
|
||||
if (postsCount || channelHasMessages || channelRefreshingFailed) {
|
||||
this.channelLoaded();
|
||||
}
|
||||
};
|
||||
|
||||
channelLoaded = () => {
|
||||
this.setState({channelLoaded: true});
|
||||
return props.posts.slice(0, props.postVisibility);
|
||||
};
|
||||
|
||||
toggleRetryMessage = (show = true) => {
|
||||
@@ -120,19 +111,17 @@ class ChannelPostList extends PureComponent {
|
||||
};
|
||||
|
||||
goToThread = (post) => {
|
||||
const {actions, channel, intl, navigator, theme} = this.props;
|
||||
const channelId = post.channel_id;
|
||||
const {actions, channelId, channelDisplayName, channelType, intl, navigator, theme} = this.props;
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
actions.loadThreadIfNecessary(post.root_id, channelId);
|
||||
actions.selectPost(rootId);
|
||||
|
||||
let title;
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
if (channelType === General.DM_CHANNEL) {
|
||||
title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
|
||||
} else {
|
||||
const channelName = channel.display_name;
|
||||
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName});
|
||||
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: channelDisplayName});
|
||||
}
|
||||
|
||||
const options = {
|
||||
@@ -161,29 +150,28 @@ class ChannelPostList extends PureComponent {
|
||||
|
||||
loadMorePosts = () => {
|
||||
if (this.state.showLoadMore) {
|
||||
const {actions, channel} = this.props;
|
||||
actions.increasePostVisibility(channel.id);
|
||||
const {actions, channelId} = this.props;
|
||||
actions.increasePostVisibility(channelId);
|
||||
}
|
||||
};
|
||||
|
||||
loadPosts = (channelId) => {
|
||||
this.setState({channelLoaded: false});
|
||||
this.props.actions.loadPostsIfNecessaryWithRetry(channelId);
|
||||
};
|
||||
|
||||
loadPostsRetry = () => {
|
||||
this.loadPosts(this.props.channel.id);
|
||||
this.loadPosts(this.props.channelId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions,
|
||||
channel,
|
||||
channelIsLoading,
|
||||
channelId,
|
||||
channelIsRefreshing,
|
||||
channelRefreshingFailed,
|
||||
currentUserId,
|
||||
lastViewedAt,
|
||||
loadingPosts,
|
||||
myMember,
|
||||
navigator,
|
||||
posts,
|
||||
theme
|
||||
@@ -203,8 +191,6 @@ class ChannelPostList extends PureComponent {
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (channelIsLoading) {
|
||||
component = <ChannelLoader theme={theme}/>;
|
||||
} else {
|
||||
component = (
|
||||
<PostList
|
||||
@@ -216,12 +202,11 @@ class ChannelPostList extends PureComponent {
|
||||
onRefresh={actions.setChannelRefreshing}
|
||||
renderReplies={true}
|
||||
indicateNewMessages={true}
|
||||
currentUserId={myMember.user_id}
|
||||
lastViewedAt={myMember.last_viewed_at}
|
||||
channel={channel}
|
||||
currentUserId={currentUserId}
|
||||
lastViewedAt={lastViewedAt}
|
||||
channelId={channelId}
|
||||
navigator={navigator}
|
||||
refreshing={channelIsRefreshing}
|
||||
channelIsLoading={channelIsLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
import {getConnection} from 'app/selectors/device';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
@@ -40,16 +41,21 @@ function makeMapStateToProps() {
|
||||
channelRefreshingFailed = false;
|
||||
}
|
||||
|
||||
const channel = getChannel(state, {id: channelId}) || {};
|
||||
|
||||
return {
|
||||
channel: getChannel(state, {id: channelId}),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
channelId,
|
||||
channelIsRefreshing,
|
||||
channelRefreshingFailed,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
channelType: channel.type,
|
||||
channelDisplayName: channel.display_name,
|
||||
posts,
|
||||
postVisibility: state.views.channel.postVisibility[channelId],
|
||||
loadingPosts: state.views.channel.loadingPosts[channelId],
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
lastViewedAt: getMyCurrentChannelMembership(state).last_viewed_at,
|
||||
networkOnline,
|
||||
totalMessageCount: channel.total_msg_count,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {selectFirstAvailableTeam} from 'app/actions/views/select_team';
|
||||
import {getStatusBarHeight} from 'app/selectors/device';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
@@ -46,12 +47,14 @@ function mapDispatchToProps(dispatch) {
|
||||
connection,
|
||||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
markChannelAsRead,
|
||||
selectFirstAvailableTeam,
|
||||
selectInitialChannel,
|
||||
initWebSocket,
|
||||
closeWebSocket,
|
||||
startPeriodicStatusUpdates,
|
||||
stopPeriodicStatusUpdates
|
||||
stopPeriodicStatusUpdates,
|
||||
viewChannel
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -289,6 +289,7 @@ class CreateChannel extends PureComponent {
|
||||
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
underlineColorAndroid='transparent'
|
||||
/>
|
||||
</View>
|
||||
@@ -322,6 +323,7 @@ class CreateChannel extends PureComponent {
|
||||
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
onFocus={this.scrollToEnd}
|
||||
underlineColorAndroid='transparent'
|
||||
/>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default class ImagePreview extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.scrollView.scrollTo({x: (this.state.currentFile) * this.state.deviceWidth, animated: false});
|
||||
this.scrollView.scrollTo({x: (this.state.currentFile) * this.props.deviceWidth, animated: false});
|
||||
Animated.timing(this.state.wrapperViewOpacity, {
|
||||
toValue: 1,
|
||||
duration: 100
|
||||
@@ -93,7 +93,7 @@ export default class ImagePreview extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.deviceWidth !== nextProps.deviceWidth && Platform.OS === 'android') {
|
||||
if (this.props.deviceWidth !== nextProps.deviceWidth) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.scrollView.scrollTo({x: (this.state.currentFile * nextProps.deviceWidth), animated: false});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ export function registerScreens(store, Provider) {
|
||||
Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider);
|
||||
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
|
||||
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
|
||||
Navigation.registerComponent('LoadTeam', () => wrapWithContextProvider(LoadTeam), store, Provider);
|
||||
Navigation.registerComponent('LoadTeam', () => wrapWithContextProvider(LoadTeam, false), store, Provider);
|
||||
Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider);
|
||||
Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(LoginOptions), store, Provider);
|
||||
Navigation.registerComponent('MFA', () => wrapWithContextProvider(Mfa), store, Provider);
|
||||
|
||||
@@ -80,7 +80,7 @@ export default class Notification extends PureComponent {
|
||||
} else if (user) {
|
||||
icon = (
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
userId={user.id}
|
||||
size={IMAGE_SIZE}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Keyboard,
|
||||
InteractionManager,
|
||||
Platform,
|
||||
SectionList,
|
||||
StyleSheet,
|
||||
@@ -97,13 +98,13 @@ class Search extends Component {
|
||||
const recentLenght = recent.length;
|
||||
const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED);
|
||||
|
||||
if (shouldScroll && !this.state.isFocused) {
|
||||
setTimeout(() => {
|
||||
if (shouldScroll) {
|
||||
requestAnimationFrame(() => {
|
||||
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
|
||||
animated: true,
|
||||
offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght + 1) * RECENT_SEPARATOR_HEIGHT)
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ class Search extends Component {
|
||||
|
||||
cancelSearch = () => {
|
||||
const {navigator} = this.props;
|
||||
this.handleTextChanged('');
|
||||
this.handleTextChanged('', true);
|
||||
navigator.dismissModal({animationType: 'slide-down'});
|
||||
};
|
||||
|
||||
@@ -161,7 +162,7 @@ class Search extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleTextChanged = (value) => {
|
||||
handleTextChanged = (value, selectionChanged) => {
|
||||
const {actions, searchingStatus} = this.props;
|
||||
this.setState({value});
|
||||
actions.handleSearchDraftChanged(value);
|
||||
@@ -170,6 +171,18 @@ class Search extends Component {
|
||||
actions.clearSearch();
|
||||
this.scrollToTop();
|
||||
}
|
||||
|
||||
// FIXME: Workaround for iOS when setting the value directly
|
||||
// in the inputText, bug in RN 0.48
|
||||
if (Platform.OS === 'ios' && selectionChanged) {
|
||||
this.handleSelectionChange({
|
||||
nativeEvent: {
|
||||
selection: {
|
||||
end: value.length
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
keyModifierExtractor = (item) => {
|
||||
@@ -393,18 +406,35 @@ class Search extends Component {
|
||||
|
||||
search = (terms, isOrSearch) => {
|
||||
const {actions, currentTeamId} = this.props;
|
||||
actions.searchPosts(currentTeamId, terms, isOrSearch);
|
||||
actions.searchPosts(currentTeamId, terms.trim(), isOrSearch);
|
||||
|
||||
this.handleTextChanged(`${terms} `);
|
||||
|
||||
// Trigger onSelectionChanged Manually when submitting
|
||||
this.handleSelectionChange({
|
||||
nativeEvent: {
|
||||
selection: {
|
||||
end: terms.length + 1
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setModifierValue = (modifier) => {
|
||||
const {value} = this.state;
|
||||
let newValue = '';
|
||||
|
||||
if (!value) {
|
||||
this.handleTextChanged(modifier);
|
||||
newValue = modifier;
|
||||
} else if (value.endsWith(' ')) {
|
||||
this.handleTextChanged(`${value}${modifier}`);
|
||||
newValue = `${value}${modifier}`;
|
||||
} else {
|
||||
this.handleTextChanged(`${value} ${modifier}`);
|
||||
newValue = `${value} ${modifier}`;
|
||||
}
|
||||
|
||||
this.handleTextChanged(newValue, true);
|
||||
|
||||
this.refs.searchBar.focus();
|
||||
};
|
||||
|
||||
setRecentValue = (recent) => {
|
||||
@@ -430,6 +460,8 @@ class Search extends Component {
|
||||
viewChannel
|
||||
} = actions;
|
||||
|
||||
setChannelLoading();
|
||||
|
||||
const channel = channels.find((c) => c.id === channelId);
|
||||
let displayName = '';
|
||||
|
||||
@@ -440,10 +472,11 @@ class Search extends Component {
|
||||
this.props.navigator.dismissModal({animationType: 'none'});
|
||||
|
||||
markChannelAsRead(channelId, currentChannelId);
|
||||
setChannelLoading();
|
||||
viewChannel(channelId, currentChannelId);
|
||||
setChannelDisplayName(displayName);
|
||||
handleSelectChannel(channelId);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
handleSelectChannel(channelId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -585,11 +618,6 @@ class Search extends Component {
|
||||
backArrowSize={28}
|
||||
/>
|
||||
</View>
|
||||
<Autocomplete
|
||||
ref={this.attachAutocomplete}
|
||||
onChangeText={this.handleTextChanged}
|
||||
isSearch={true}
|
||||
/>
|
||||
<SectionList
|
||||
ref='list'
|
||||
style={style.sectionList}
|
||||
@@ -599,6 +627,11 @@ class Search extends Component {
|
||||
keyboardDismissMode='interactive'
|
||||
stickySectionHeadersEnabled={Platform.OS === 'ios'}
|
||||
/>
|
||||
<Autocomplete
|
||||
ref={this.attachAutocomplete}
|
||||
onChangeText={this.handleTextChanged}
|
||||
isSearch={true}
|
||||
/>
|
||||
{previewComponent}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
InteractionManager,
|
||||
Platform,
|
||||
Text,
|
||||
View,
|
||||
WebView
|
||||
@@ -20,7 +21,7 @@ import StatusBar from 'app/components/status_bar';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const jsCode = 'window.postMessage(document.body.innerText)';
|
||||
const jsCode = "setTimeout(function() { postMessage(document.body.innerText, '*')})";
|
||||
|
||||
class SSO extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -114,9 +115,15 @@ class SSO extends PureComponent {
|
||||
};
|
||||
|
||||
onNavigationStateChange = (navState) => {
|
||||
const {url, navigationType} = navState;
|
||||
const {url, navigationType, loading} = navState;
|
||||
let submitted = false;
|
||||
if (Platform.OS === 'ios') {
|
||||
submitted = url.includes(this.completedUrl) && navigationType === 'formsubmit';
|
||||
} else {
|
||||
submitted = url.includes(this.completedUrl) && loading;
|
||||
}
|
||||
|
||||
if (url.includes(this.completedUrl) && navigationType === 'formsubmit') {
|
||||
if (submitted) {
|
||||
this.setState({onMessage: this.onMessage});
|
||||
}
|
||||
};
|
||||
@@ -125,7 +132,7 @@ class SSO extends PureComponent {
|
||||
const url = event.nativeEvent.url;
|
||||
|
||||
if (url.includes(this.completedUrl)) {
|
||||
CookieManager.get(this.props.serverUrl, (err, res) => {
|
||||
CookieManager.get(this.props.serverUrl).then((res) => {
|
||||
const token = res.MMAUTHTOKEN;
|
||||
|
||||
if (token) {
|
||||
@@ -146,6 +153,10 @@ class SSO extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
renderLoading = () => {
|
||||
return <Loading/>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const {error, renderWebView} = this.state;
|
||||
@@ -153,9 +164,7 @@ class SSO extends PureComponent {
|
||||
|
||||
let content;
|
||||
if (!renderWebView) {
|
||||
content = (
|
||||
<Loading/>
|
||||
);
|
||||
content = this.renderLoading();
|
||||
} else if (error) {
|
||||
content = (
|
||||
<View style={style.errorContainer}>
|
||||
@@ -172,7 +181,7 @@ class SSO extends PureComponent {
|
||||
startInLoadingState={true}
|
||||
onNavigationStateChange={this.onNavigationStateChange}
|
||||
onShouldStartLoadWithRequest={() => true}
|
||||
renderLoading={() => (<Loading/>)}
|
||||
renderLoading={this.renderLoading}
|
||||
onMessage={this.state.onMessage}
|
||||
injectedJavaScript={jsCode}
|
||||
onLoadEnd={this.onLoadEnd}
|
||||
|
||||
@@ -115,7 +115,7 @@ class UserProfile extends PureComponent {
|
||||
>
|
||||
<View style={style.top}>
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
userId={user.id}
|
||||
size={150}
|
||||
statusBorderWidth={6}
|
||||
statusSize={40}
|
||||
|
||||
226
app/selectors/autocomplete.js
Normal file
226
app/selectors/autocomplete.js
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {
|
||||
getCurrentUser, getCurrentUserId, getProfilesInCurrentChannel,
|
||||
getProfilesNotInCurrentChannel, getProfilesInCurrentTeam
|
||||
} from 'mattermost-redux/selectors/entities/users';
|
||||
import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {sortByUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import * as Autocomplete from 'app/constants/autocomplete';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
|
||||
export const getMatchTermForAtMention = (() => {
|
||||
let lastMatchTerm = null;
|
||||
let lastValue;
|
||||
let lastIsSearch;
|
||||
return (value, isSearch) => {
|
||||
if (value !== lastValue || isSearch !== lastIsSearch) {
|
||||
const regex = isSearch ? Autocomplete.AT_MENTION_SEARCH_REGEX : Autocomplete.AT_MENTION_REGEX;
|
||||
const match = value.match(regex);
|
||||
lastValue = value;
|
||||
lastIsSearch = isSearch;
|
||||
if (match) {
|
||||
lastMatchTerm = isSearch ? match[1] : match[2];
|
||||
} else {
|
||||
lastMatchTerm = null;
|
||||
}
|
||||
}
|
||||
return lastMatchTerm;
|
||||
};
|
||||
})();
|
||||
|
||||
export const getMatchTermForChannelMention = (() => {
|
||||
let lastMatchTerm = null;
|
||||
let lastValue;
|
||||
let lastIsSearch;
|
||||
return (value, isSearch) => {
|
||||
if (value !== lastValue || isSearch !== lastIsSearch) {
|
||||
const regex = isSearch ? Autocomplete.CHANNEL_MENTION_SEARCH_REGEX : Autocomplete.CHANNEL_MENTION_REGEX;
|
||||
const match = value.match(regex);
|
||||
lastValue = value;
|
||||
lastIsSearch = isSearch;
|
||||
if (match) {
|
||||
lastMatchTerm = isSearch ? match[1] : match[2];
|
||||
} else {
|
||||
lastMatchTerm = null;
|
||||
}
|
||||
}
|
||||
return lastMatchTerm;
|
||||
};
|
||||
})();
|
||||
|
||||
export const filterMembersInChannel = createSelector(
|
||||
getProfilesInCurrentChannel,
|
||||
getCurrentUserId,
|
||||
(state, matchTerm) => matchTerm,
|
||||
(profilesInChannel, currentUserId, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let profiles;
|
||||
if (matchTerm) {
|
||||
profiles = profilesInChannel.filter((p) => {
|
||||
return ((p.id !== currentUserId) && (
|
||||
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
|
||||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
|
||||
});
|
||||
} else {
|
||||
profiles = profilesInChannel.filter((p) => p.id !== currentUserId);
|
||||
}
|
||||
|
||||
// already sorted
|
||||
return profiles.map((p) => p.id);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterMembersNotInChannel = createSelector(
|
||||
getProfilesNotInCurrentChannel,
|
||||
getCurrentUserId,
|
||||
(state, matchTerm) => matchTerm,
|
||||
(profilesNotInChannel, currentUserId, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let profiles;
|
||||
if (matchTerm) {
|
||||
profiles = profilesNotInChannel.filter((p) => {
|
||||
return ((p.id !== currentUserId) && (
|
||||
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
|
||||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
|
||||
});
|
||||
} else {
|
||||
profiles = profilesNotInChannel;
|
||||
}
|
||||
|
||||
return profiles.map((p) => p.id);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterMembersInCurrentTeam = createSelector(
|
||||
getProfilesInCurrentTeam,
|
||||
getCurrentUser,
|
||||
(state, matchTerm) => matchTerm,
|
||||
(profilesInTeam, currentUser, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIXME: We need to include the currentUser here as is not in profilesInTeam on the redux store
|
||||
let profiles;
|
||||
if (matchTerm) {
|
||||
profiles = [...profilesInTeam, currentUser].filter((p) => {
|
||||
return (p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
|
||||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm));
|
||||
});
|
||||
} else {
|
||||
profiles = [...profilesInTeam, currentUser];
|
||||
}
|
||||
|
||||
return profiles.sort(sortByUsername).map((p) => p.id);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterMyChannels = createSelector(
|
||||
getMyChannels,
|
||||
(state, opts) => opts,
|
||||
(myChannels, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let channels;
|
||||
if (matchTerm) {
|
||||
channels = myChannels.filter((c) => {
|
||||
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL) &&
|
||||
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
|
||||
});
|
||||
} else {
|
||||
channels = myChannels.filter((c) => {
|
||||
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL);
|
||||
});
|
||||
}
|
||||
|
||||
return channels.map((c) => c.id);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterOtherChannels = createSelector(
|
||||
getOtherChannels,
|
||||
(state, matchTerm) => matchTerm,
|
||||
(otherChannels, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let channels;
|
||||
if (matchTerm) {
|
||||
channels = otherChannels.filter((c) => {
|
||||
return (c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
|
||||
});
|
||||
} else {
|
||||
channels = otherChannels;
|
||||
}
|
||||
|
||||
return channels.map((c) => c.id);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterPublicChannels = createSelector(
|
||||
getMyChannels,
|
||||
getOtherChannels,
|
||||
getCurrentLocale,
|
||||
(state, matchTerm) => matchTerm,
|
||||
(myChannels, otherChannels, locale, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let channels;
|
||||
if (matchTerm) {
|
||||
channels = myChannels.filter((c) => {
|
||||
return c.type === General.OPEN_CHANNEL &&
|
||||
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
|
||||
}).concat(
|
||||
otherChannels.filter((c) => c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm))
|
||||
);
|
||||
} else {
|
||||
channels = myChannels.filter((c) => {
|
||||
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL);
|
||||
}).concat(otherChannels);
|
||||
}
|
||||
|
||||
return channels.sort(sortChannelsByDisplayName.bind(null, locale)).map((c) => c.id);
|
||||
}
|
||||
);
|
||||
|
||||
export const filterPrivateChannels = createSelector(
|
||||
getMyChannels,
|
||||
(state, matchTerm) => matchTerm,
|
||||
(myChannels, matchTerm) => {
|
||||
if (matchTerm === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let channels;
|
||||
if (matchTerm) {
|
||||
channels = myChannels.filter((c) => {
|
||||
return c.type === General.PRIVATE_CHANNEL &&
|
||||
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
|
||||
});
|
||||
} else {
|
||||
channels = myChannels.filter((c) => {
|
||||
return c.type === General.PRIVATE_CHANNEL;
|
||||
});
|
||||
}
|
||||
|
||||
return channels.map((c) => c.id);
|
||||
}
|
||||
);
|
||||
@@ -46,8 +46,10 @@ export const getTheme = createSelector(
|
||||
// At this point, the theme should be a plain object
|
||||
|
||||
// Fix a case where upper case theme colours are rendered as black
|
||||
for (const key of Object.keys(theme)) {
|
||||
theme[key] = theme[key].toLowerCase();
|
||||
if (currentTeamId) {
|
||||
for (const key of Object.keys(theme)) {
|
||||
theme[key] = theme[key].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
|
||||
23
app/selectors/teams.js
Normal file
23
app/selectors/teams.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
export const getMySortedTeams = createSelector(
|
||||
getMyTeams,
|
||||
getCurrentUser,
|
||||
(teams, currentUser) => {
|
||||
const locale = currentUser.locale;
|
||||
|
||||
return teams.sort((a, b) => {
|
||||
if (a.display_name !== b.display_name) {
|
||||
return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true});
|
||||
}
|
||||
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
const emptyDraft = {
|
||||
draft: '',
|
||||
@@ -20,9 +20,9 @@ function getThreadDrafts(state) {
|
||||
|
||||
export const getCurrentChannelDraft = createSelector(
|
||||
getChannelDrafts,
|
||||
getCurrentChannel,
|
||||
(drafts, currentChannel) => {
|
||||
return drafts[currentChannel.id] || emptyDraft;
|
||||
getCurrentChannelId,
|
||||
(drafts, currentChannelId) => {
|
||||
return drafts[currentChannelId] || emptyDraft;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -48,13 +48,13 @@ export default function configureAppStore(initialState) {
|
||||
['typing']
|
||||
);
|
||||
|
||||
const channelViewBlackList = {loading: true, refreshing: true, tooltipVisible: true, postVisibility: true, loadingPosts: true};
|
||||
const channelViewBlackList = {loading: true, refreshing: true, tooltipVisible: true, loadingPosts: true};
|
||||
const channelViewBlackListFilter = createTransform(
|
||||
(inboundState) => {
|
||||
const channel = {};
|
||||
|
||||
for (const channelKey of Object.keys(inboundState.channel)) {
|
||||
if (channelViewBlackList[channelKey]) {
|
||||
if (!channelViewBlackList[channelKey]) {
|
||||
channel[channelKey] = inboundState.channel[channelKey];
|
||||
}
|
||||
}
|
||||
|
||||
49
app/utils/why_did_you_update.js
Normal file
49
app/utils/why_did_you_update.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// Gist created by https://benchling.engineering/a-deep-dive-into-react-perf-debugging-fd2063f5a667
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
function isRequiredUpdateObject(o) {
|
||||
return Array.isArray(o) || (o && o.constructor === Object.prototype.constructor);
|
||||
}
|
||||
|
||||
function deepDiff(o1, o2, p) {
|
||||
const notify = (status) => {
|
||||
console.warn('Update %s', status);
|
||||
console.log('%cbefore', 'font-weight: bold', o1);
|
||||
console.log('%cafter ', 'font-weight: bold', o2);
|
||||
};
|
||||
if (!_.isEqual(o1, o2)) {
|
||||
console.group(p);
|
||||
if ([o1, o2].every(_.isFunction)) {
|
||||
notify('avoidable?');
|
||||
} else if (![o1, o2].every(isRequiredUpdateObject)) {
|
||||
notify('required.');
|
||||
} else {
|
||||
const keys = _.union(_.keys(o1), _.keys(o2));
|
||||
for (const key of keys) {
|
||||
deepDiff(o1[key], o2[key], key);
|
||||
}
|
||||
}
|
||||
console.groupEnd();
|
||||
} else if (o1 !== o2) {
|
||||
console.group(p);
|
||||
notify('avoidable!');
|
||||
if (_.isObject(o1) && _.isObject(o2)) {
|
||||
const keys = _.union(_.keys(o1), _.keys(o2));
|
||||
for (const key of keys) {
|
||||
deepDiff(o1[key], o2[key], key);
|
||||
}
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
function whyDidYouUpdate(prevProps, prevState) {
|
||||
deepDiff({props: prevProps, state: prevState},
|
||||
{props: this.props, state: this.state},
|
||||
this.constructor.name);
|
||||
}
|
||||
|
||||
export default whyDidYouUpdate;
|
||||
@@ -234,7 +234,7 @@
|
||||
"admin.customization.enableCustomEmojiTitle": "Eigene Emoji ermöglichen:",
|
||||
"admin.customization.enableEmojiPickerDesc": "Die Emojiauswahl ermöglicht den Benutzern Emojis als Reaktionen oder zur Verwendung in Nachrichten auszuwählen. Die Aktivierung der Emojiauswahl bei einer großen Anzahl an benutzerdefinierten Emojis kann die Performance verringern.",
|
||||
"admin.customization.enableEmojiPickerTitle": "Emojiauswahl aktivieren:",
|
||||
"admin.customization.enableLinkPreviewsDesc": "Erlaube Benutzern eine Vorschau einer Webseite unterhalb der Nachricht anzuzeigen, sofern verfügbar. Wenn wahr, können Webseitenvorschauen über Kontoeinstellungen > Erweitert > Vorschau auf Features der neuen Version aktiviert werden.",
|
||||
"admin.customization.enableLinkPreviewsDesc": "Erlaube Benutzern eine Vorschau einer Webseite unterhalb der Nachricht anzuzeigen, sofern verfügbar. Wenn wahr, können Webseitenvorschauen über Kontoeinstellungen > Erweitert > Vorschau auf Funktionen der neuen Version aktiviert werden.",
|
||||
"admin.customization.enableLinkPreviewsTitle": "Erlaube Link-Vorschauen:",
|
||||
"admin.customization.iosAppDownloadLinkDesc": "Einen Link zum Download der iOS-App hinzufügen. Benutzer, die die Seite über einen mobilen Browser aufrufen, wird eine Seite mit der Option die App herunterzuladen angezeigt. Dieses Feld leer lassen um zu verhindern, dass die Seite angezeigt wird.",
|
||||
"admin.customization.iosAppDownloadLinkTitle": "iOS-App-Downloadlink:",
|
||||
@@ -580,7 +580,7 @@
|
||||
"admin.log.consoleDescription": "Üblicherweise falsch bei Produktionsumgebungen. Entwickler können dieses auf wahr stellen um Logmeldungen auf der Konsole anhand der Konsolen-Level-Option zu erhalten. Wenn wahr, schreibt der Server die Meldungen in den Standard-Ausgabe-Stream (stdout).",
|
||||
"admin.log.consoleTitle": "Logs auf der Konsole ausgeben: ",
|
||||
"admin.log.enableDiagnostics": "Aktiviere Diagnose und Fehlerübermittlung:",
|
||||
"admin.log.enableDiagnosticsDescription": "Durch aktivieren dieses Features kann die Qualität und Performance von Mattermost verbessert werden indem Diagnose und Fehler an Mattermost, Inc übermittelt werden. Lesen Sie unsere <a href=\"https://about.mattermost.com/default-privacy-policy/\" target='_blank'>Datenschutzbestimmungen</a> um mehr darüber zu erfahren.",
|
||||
"admin.log.enableDiagnosticsDescription": "Durch aktivieren dieser Funktion kann die Qualität und Performance von Mattermost verbessert werden indem Diagnose und Fehler an Mattermost, Inc übermittelt werden. Lesen Sie unsere <a href=\"https://about.mattermost.com/default-privacy-policy/\" target='_blank'>Datenschutzbestimmungen</a> um mehr darüber zu erfahren.",
|
||||
"admin.log.enableWebhookDebugging": "Aktiviere Webhook-Debugging:",
|
||||
"admin.log.enableWebhookDebuggingDescription": "Sie können dies auf falsch setzen, um das Debug-Logging von eingehenden Webhook-Anfragen zu deaktivieren.",
|
||||
"admin.log.fileDescription": "Normalerweise wahr in Produktionsumgebungen. Wenn wahr, werden Ereignisse in die mattermost.log in den unter Log-Verzeichnis angegebene Ordner mitgeschrieben. Die Logs werden bei 10.000 Zeilen rotiert, in eine Datei im selben Verzeichnis archiviert und mit einem Datumsstempel und Seriennummer versehen. Zum Beispiel mattermost.2017-03-31.001.",
|
||||
@@ -1927,18 +1927,18 @@
|
||||
"mobile.notification_settings_mentions.keywordsDescription": "Andere Wörter, die eine Erwähnung auslösen",
|
||||
"mobile.notification_settings_mentions.keywordsHelp": "Stichwörter sind nicht groß-/kleinschreibungsabhängig und sollten durch ein Komma getrennt sein",
|
||||
"mobile.notification_settings_mentions.wordsTrigger": "WÖRTER, DIE ERWÄHNUNGEN AUSLÖSEN",
|
||||
"mobile.notification_settings_mobile.default_sound": "Default ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "Light",
|
||||
"mobile.notification_settings_mobile.default_sound": "Standard ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "Licht",
|
||||
"mobile.notification_settings_mobile.no_sound": "Keine",
|
||||
"mobile.notification_settings_mobile.push_activity": "SENDE BENACHRICHTIGUNGEN FÜR",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Sende Benachrichtigungen für",
|
||||
"mobile.notification_settings_mobile.push_status": "PUSH-BENACHRICHTIGUNGEN AUSLÖSEN WENN",
|
||||
"mobile.notification_settings_mobile.push_status_android": "Push-Benachrichtigungen auslösen wenn",
|
||||
"mobile.notification_settings_mobile.sound": "Sound",
|
||||
"mobile.notification_settings_mobile.push_status": "PUSH-BENACHRICHTIGUNGEN AUSLÖSEN, WENN",
|
||||
"mobile.notification_settings_mobile.push_status_android": "Push-Benachrichtigungen auslösen, wenn",
|
||||
"mobile.notification_settings_mobile.sound": "Ton",
|
||||
"mobile.notification_settings_mobile.sounds_title": "Benachrichtigungston",
|
||||
"mobile.notification_settings_mobile.test": "Send me a test notification",
|
||||
"mobile.notification_settings_mobile.test_push": "This is a test push notification",
|
||||
"mobile.notification_settings_mobile.vibrate": "Vibrate",
|
||||
"mobile.notification_settings_mobile.test": "Schicke mir eine Testbenachrichtigung",
|
||||
"mobile.notification_settings_mobile.test_push": "Dies ist eine Test-Push-Benachrichtigung",
|
||||
"mobile.notification_settings_mobile.vibrate": "Vibrieren",
|
||||
"mobile.offlineIndicator.connected": "Verbunden",
|
||||
"mobile.offlineIndicator.connecting": "Verbinde...",
|
||||
"mobile.offlineIndicator.offline": "Keine Internetverbindung",
|
||||
@@ -1953,7 +1953,7 @@
|
||||
"mobile.post.retry": "Aktualisieren",
|
||||
"mobile.post_info.add_reaction": "Reaktion hinzufügen",
|
||||
"mobile.request.invalid_response": "Ungültige Antwort vom Server erhalten.",
|
||||
"mobile.retry_message": "Refreshing messages failed. Pull up to try again.",
|
||||
"mobile.retry_message": "Aktualisierung der Nachrichten fehlgeschlagen. Nach oben ziehen, um erneut zu versuchen.",
|
||||
"mobile.routes.channelInfo": "Info",
|
||||
"mobile.routes.channelInfo.createdBy": "Erstellt durch {creator} am ",
|
||||
"mobile.routes.channelInfo.delete_channel": "Kanal löschen",
|
||||
@@ -2423,7 +2423,7 @@
|
||||
"upload_overlay.info": "Datei zum Hochladen hier ablegen.",
|
||||
"user.settings.advance.embed_preview": "Für den ersten Weblink in einer Nachricht eine Vorschau des Webseiteninhaltes unterhalb der Nachricht anzeigen, sofern verfügbar",
|
||||
"user.settings.advance.embed_toggle": "Zeige Umschalter für alle eingebetteten Vorschauen",
|
||||
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} aktiviert",
|
||||
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Funktion} other {Funktionen}} aktiviert",
|
||||
"user.settings.advance.formattingDesc": "Wenn aktiviert, werden Nachrichten so formatiert, dass Links erstellt, Emojis angezeigt, Text formatiert und Zeilenumbrüche hinzugefügt werden. Standardmäßig ist dies aktiviert. Ändern der Einstellung erfordert ein Neuladen der Seite.",
|
||||
"user.settings.advance.formattingTitle": "Formatierung von Nachrichten aktivieren",
|
||||
"user.settings.advance.joinLeaveDesc": "Wenn \"Ein\" werden Systemnachrichten angezeigt wenn ein Benutzer einen Kanal betritt oder verlässt. Wenn \"Aus\" werden diese Nachrichten nicht angezeigt. Es wird weiterhin eine Meldung angezeigt wenn Sie einem Kanal hinzugefügt werden sodass Sie Benachrichtigungen erhalten können.",
|
||||
@@ -2431,8 +2431,8 @@
|
||||
"user.settings.advance.markdown_preview": "Zeige Markdown Vorschauoption im der Nachrichten Eingabefeld",
|
||||
"user.settings.advance.off": "Aus",
|
||||
"user.settings.advance.on": "Ein",
|
||||
"user.settings.advance.preReleaseDesc": "Markieren Sie jede Pre-Release-Features, die Sie testen möchten. Eventuell müssen Sie die Seite neu laden, bevor die Änderung wirksam werden.",
|
||||
"user.settings.advance.preReleaseTitle": "Vorschau auf Features der neuen Version",
|
||||
"user.settings.advance.preReleaseDesc": "Markieren Sie jede Pre-Release-Funktionen, die Sie testen möchten. Eventuell müssen Sie die Seite neu laden, bevor die Änderung wirksam werden.",
|
||||
"user.settings.advance.preReleaseTitle": "Vorschau auf Funktionen der neuen Version",
|
||||
"user.settings.advance.sendDesc": "Wenn aktiviert, fügt Enter einen Zeilenumbruch ein und Strg+Enter sendet die Nachricht.",
|
||||
"user.settings.advance.sendTitle": "Sende Nachrichten mit Strg+Enter",
|
||||
"user.settings.advance.slashCmd_autocmp": "Erlaube externe Anwendung Slash-Befehl-Autovervollständigung anzubieten",
|
||||
@@ -2485,7 +2485,7 @@
|
||||
"user.settings.display.preferTime": "Wählen Sie das bevorzugte Zeitformat aus.",
|
||||
"user.settings.display.theme.applyToAllTeams": "Motiv für alle meine Teams festlegen",
|
||||
"user.settings.display.theme.customTheme": "Benutzerdefiniertes Motiv",
|
||||
"user.settings.display.theme.describe": "Öffnen um das Motiv zu ändern",
|
||||
"user.settings.display.theme.describe": "Öffnen, um das Motiv zu ändern",
|
||||
"user.settings.display.theme.import": "Importiere Motivfarben von Slack",
|
||||
"user.settings.display.theme.otherThemes": "Zeige andere Themes",
|
||||
"user.settings.display.theme.themeColors": "Motivfarben",
|
||||
@@ -2747,7 +2747,7 @@
|
||||
"webrtc.cancel": "Anruf abbrechen",
|
||||
"webrtc.cancelled": "{username} hat den Anruf abgebrochen.",
|
||||
"webrtc.declined": "Ihr Anruf wurde durch {username} abgelehnt.",
|
||||
"webrtc.disabled": "{username} hat WebRTC deaktiviert. Um die Funktion zu aktivieren, muss der Benutzer zu Kontoeinstellungen > Erweitert > Vorschau auf Features der neuen Version gehen und WebRTC aktivieren.",
|
||||
"webrtc.disabled": "{username} hat WebRTC deaktiviert. Um die Funktion zu aktivieren, muss der Benutzer zu Kontoeinstellungen > Erweitert > Vorschau auf Funktionen der neuen Version gehen und WebRTC aktivieren.",
|
||||
"webrtc.failed": "Es gab ein Problem bei der Verbindung des Videoanrufs.",
|
||||
"webrtc.hangup": "Auflegen",
|
||||
"webrtc.header": "Anruf mit {username}",
|
||||
|
||||
@@ -1930,8 +1930,8 @@
|
||||
"mobile.notification_settings_mobile.default_sound": "Predeterminado ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "Luz",
|
||||
"mobile.notification_settings_mobile.no_sound": "Ninguno",
|
||||
"mobile.notification_settings_mobile.push_activity": "ENVIAR NOTIFICACIONES PARA",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Enviar notificaciones para",
|
||||
"mobile.notification_settings_mobile.push_activity": "ENVIAR NOTIFICACIONES",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Enviar notificaciones",
|
||||
"mobile.notification_settings_mobile.push_status": "ACTIVA LAS NOTIFICACIONES PUSH CUANDO",
|
||||
"mobile.notification_settings_mobile.push_status_android": "Activa las notificaciones push cuando",
|
||||
"mobile.notification_settings_mobile.sound": "Sonido",
|
||||
|
||||
@@ -1461,7 +1461,7 @@
|
||||
"error.generic.title": "Errore",
|
||||
"error.local_storage.help1": "Attiva cookies",
|
||||
"error.local_storage.help2": "Disattiva navigazione privata",
|
||||
"error.local_storage.help3": "Utilizza un browser supportato (IE 11, Chrome 43+, Firefox 38+, Safari 9, Edge)",
|
||||
"error.local_storage.help3": "Utilizza un browser supportato (IE 11, Chrome 43+, Firefox 38+, Safari 9, Edge 40+)",
|
||||
"error.local_storage.message": "Mattermos non è riuscito a caricarsi a causa di un'impostazione sul tuo browser che impedisce di utilizzare le funzioni di archiviazione locale. Per permettere a Mattermost di caricarsi, prova le seguenti azioni:",
|
||||
"error.not_found.link_message": "Torna a Mattermost",
|
||||
"error.not_found.message": "La pagina che cerchi di raggiungere non esiste",
|
||||
|
||||
@@ -1931,7 +1931,7 @@
|
||||
"mobile.notification_settings_mobile.light": "Light",
|
||||
"mobile.notification_settings_mobile.no_sound": "None",
|
||||
"mobile.notification_settings_mobile.push_activity": "SEND NOTIFICATIONS",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Send notifications",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "답글 알림",
|
||||
"mobile.notification_settings_mobile.push_status": "TRIGGER PUSH NOTIFICATIONS WHEN",
|
||||
"mobile.notification_settings_mobile.push_status_android": "지정한 상태일 때 모바일 푸시 알림",
|
||||
"mobile.notification_settings_mobile.sound": "Sound",
|
||||
|
||||
@@ -1931,7 +1931,7 @@
|
||||
"mobile.notification_settings_mobile.light": "Light",
|
||||
"mobile.notification_settings_mobile.no_sound": "Geen",
|
||||
"mobile.notification_settings_mobile.push_activity": "SEND NOTIFICATIONS",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Send notifications",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Antwoord meldingen",
|
||||
"mobile.notification_settings_mobile.push_status": "TRIGGER PUSH NOTIFICATIONS WHEN",
|
||||
"mobile.notification_settings_mobile.push_status_android": "Trigger push notificaties wanneer",
|
||||
"mobile.notification_settings_mobile.sound": "Sound",
|
||||
|
||||
@@ -475,7 +475,7 @@
|
||||
"admin.image.amazonS3IdDescription": "Obter essa credencial do seu administrador Amazon EC2.",
|
||||
"admin.image.amazonS3IdExample": "Ex.: \"AKIADTOVBGERKLCBV\"",
|
||||
"admin.image.amazonS3IdTitle": "Amazon S3 Access Key ID:",
|
||||
"admin.image.amazonS3RegionDescription": "(Optional) AWS region you selected when creating your S3 bucket. If no region is set, Mattermost attempts to get the appropriate region from AWS, or sets it to 'us-east-1' if none found.",
|
||||
"admin.image.amazonS3RegionDescription": "(Opcional) A região AWS que você selecionou quando criou o S3 bucket. Se a região não for configurada, o Mattermost irá tentar selecionar uma região apropriada na AWS, ou configurar para 'us-east-1' caso não ache.",
|
||||
"admin.image.amazonS3RegionExample": "Ex.: \"us-east-1\"",
|
||||
"admin.image.amazonS3RegionTitle": "Amazon S3 Region:",
|
||||
"admin.image.amazonS3SSEDescription": "Quando verdadeiro, criptografa arquivos no Amazon S3 usando criptografia do lado do servidor com as chaves gerenciadas do Amazon S3. Veja a <a href=\"https://about.mattermost.com/default-server-side-encryption\">documentação</a> para saber mais.",
|
||||
@@ -1607,7 +1607,7 @@
|
||||
"help.formatting.solirizedDarkTheme": "**Tema Solarized Dark**",
|
||||
"help.formatting.solirizedLightTheme": "**Tema Solarized Light**",
|
||||
"help.formatting.style": "## Estilo do Texto\n\nVocê pode usar `_` ou `*` em volta de uma palavra para deixar itálico. Use dois para deixar negrito.\n\n* `_itálico_` formata _itálico_\n* `**negrito**` formata **negrito**\n* `**_negrito-itálico_**` formata **_negrito-itálico_**\n* `~~tachado~~` formata ~~tachado~~",
|
||||
"help.formatting.supportedSyntax": "As linguagens suportadas são:\n`as`, `applescript`, `osascript`, `scpt`, `bash`, `sh`, `zsh`, `clj`, `boot`, `cl2`, `cljc`, `cljs`, `cljs.hl`, `cljscm`, `cljx`, `hic`, `coffee`, `_coffee`, `cake`, `cjsx`, `cson`, `iced`, `cpp`, `c`, `cc`, `h`, `c++`, `h++`, `hpp`, `cs`, `csharp`, `css`, `d`, `di`, `dart`, `delphi`, `dpr`, `dfm`, `pas`, `pascal`, `freepascal`, `lazarus`, `lpr`, `lfm`, `diff`, `django`, `jinja`, `dockerfile`, `docker`, `erl`, `f90`, `f95`, `fsharp`, `fs`, `gcode`, `nc`, `go`, `groovy`, `handlebars`, `hbs`, `html.hbs`, `html.handlebars`, `hs`, `hx`, `java`, `jsp`, `js`, `jsx`, `json`, `jl`, `kt`, `ktm`, `kts`, `less`, `lisp`, `lua`, `mk`, `mak`, `md`, `mkdown`, `mkd`, `matlab`, `m`, `mm`, `objc`, `obj-c`, `ml`, `perl`, `pl`, `php`, `php3`, `php4`, `php5`, `php6`, `ps`, `ps1`, `pp`, `py`, `gyp`, `r`, `ruby`, `rb`, `gemspec`, `podspec`, `thor`, `irb`, `rs`, `scala`, `scm`, `sld`, `scss`, `st`, `sql`, `swift`, `tex`, `vbnet`, `vb`, `bas`, `vbs`, `v`, `veo`, `xml`, `html`, `xhtml`, `rss`, `atom`, `xsl`, `plist`, `yaml`",
|
||||
"help.formatting.supportedSyntax": "Linguagens suportadas são:\n`as`, `applescript`, `osascript`, `scpt`, `bash`, `sh`, `zsh`, `clj`, `boot`, `cl2`, `cljc`, `cljs`, `cljs.hl`, `cljscm`, `cljx`, `hic`, `coffee`, `_coffee`, `cake`, `cjsx`, `cson`, `iced`, `cpp`, `c`, `cc`, `h`, `c++`, `h++`, `hpp`, `cs`, `csharp`, `css`, `d`, `di`, `dart`, `delphi`, `dpr`, `dfm`, `pas`, `pascal`, `freepascal`, `lazarus`, `lpr`, `lfm`, `diff`, `django`, `jinja`, `dockerfile`, `docker`, `erl`, `f90`, `f95`, `fsharp`, `fs`, `gcode`, `nc`, `go`, `groovy`, `handlebars`, `hbs`, `html.hbs`, `html.handlebars`, `hs`, `hx`, `java`, `jsp`, `js`, `jsx`, `json`, `jl`, `kt`, `ktm`, `kts`, `less`, `lisp`, `lua`, `mk`, `mak`, `md`, `mkdown`, `mkd`, `matlab`, `m`, `mm`, `objc`, `obj-c`, `ml`, `perl`, `pl`, `php`, `php3`, `php4`, `php5`, `php6`, `ps`, `ps1`, `pp`, `py`, `gyp`, `r`, `ruby`, `rb`, `gemspec`, `podspec`, `thor`, `irb`, `rs`, `scala`, `scm`, `sld`, `scss`, `st`, `styl`, `sql`, `swift`, `tex`, `vbnet`, `vb`, `bas`, `vbs`, `v`, `veo`, `xml`, `html`, `xhtml`, `rss`, `atom`, `xsl`, `plist`, `yaml`",
|
||||
"help.formatting.syntax": "### Destaque de Sintaxe\n\nPara adicionar destaque de sintaxe, digite o idioma a ser destacado após ``` no início do bloco de código. Mattermost também oferece quatro diferentes temas de código (GitHub, Solarized Dark, Solarized Light, Monokai) que podem ser alterados em **Configurações de Conta** > **Exibir** > **Tema** > **Tema Personalizado** > **Estilos Canal Central**",
|
||||
"help.formatting.syntaxEx": " package main\n import \"fmt\"\n func main() {\n fmt.Println(\"Hello, 世界\")\n }",
|
||||
"help.formatting.tableExample": "| Alinhado a Esquerda | Centralizado | Alinhado a Direita |\n| :------------ |:---------------:| -----:|\n| Coluna Esquerda 1 | este texto | $100 |\n| Coluna Esquerda 2 | é | $10 |\n| Coluna Esquerda 3 | centralizado | $1 |",
|
||||
@@ -1930,8 +1930,8 @@
|
||||
"mobile.notification_settings_mobile.default_sound": "Padrão ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "Luz",
|
||||
"mobile.notification_settings_mobile.no_sound": "Nenhum",
|
||||
"mobile.notification_settings_mobile.push_activity": "ENVIAR NOTIFICAÇÕES PARA",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Enviar notificações para",
|
||||
"mobile.notification_settings_mobile.push_activity": "ENVIAR NOTIFICAÇÕES",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Enviar notificações",
|
||||
"mobile.notification_settings_mobile.push_status": "DISPARAR NOTIFICAÇÕES PUSH QUANDO",
|
||||
"mobile.notification_settings_mobile.push_status_android": "Disparar notificações push quando",
|
||||
"mobile.notification_settings_mobile.sound": "Som",
|
||||
|
||||
@@ -1930,8 +1930,8 @@
|
||||
"mobile.notification_settings_mobile.default_sound": "Varsayılan ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "Beyaz",
|
||||
"mobile.notification_settings_mobile.no_sound": "Yok",
|
||||
"mobile.notification_settings_mobile.push_activity": "ŞUNUN İÇİN BİLDİRİMLER GÖNDERİLSİN",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Şunun için bildirimler gönderilsin",
|
||||
"mobile.notification_settings_mobile.push_activity": "BİLDİRİMLER GÖNDERİLSİN",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "Bildirimler gönderilsin",
|
||||
"mobile.notification_settings_mobile.push_status": "ŞU OLDUĞUNDA ANINDA BİLDİRİMLER TETİKLENSİN",
|
||||
"mobile.notification_settings_mobile.push_status_android": "Şu olduğunda anında bildirimler gönderilsin",
|
||||
"mobile.notification_settings_mobile.sound": "Ses",
|
||||
|
||||
@@ -1458,10 +1458,10 @@
|
||||
"error.generic.link": "回到 Mattermost",
|
||||
"error.generic.link_message": "回到 Mattermost",
|
||||
"error.generic.message": "发生了错误。",
|
||||
"error.generic.title": "Error",
|
||||
"error.generic.title": "错误",
|
||||
"error.local_storage.help1": "开启 cookies",
|
||||
"error.local_storage.help2": "关闭隐私浏览",
|
||||
"error.local_storage.help3": "使用一个支持的浏览器 (IE 11, Chrome 43+, Firefox 38+, Safari 9, Edge)",
|
||||
"error.local_storage.help3": "使用一个支持的浏览器 (IE 11, Chrome 43+, Firefox 52+, Safari 9, Edge 40+)",
|
||||
"error.local_storage.message": "Mattermost 无法加载因为您的浏览器设置禁止使用本地储存功能。请尝试以下操作以让 Mattermost 加载:",
|
||||
"error.not_found.link_message": "返回Mattermost",
|
||||
"error.not_found.message": "您访问的页面不存在",
|
||||
@@ -1607,7 +1607,7 @@
|
||||
"help.formatting.solirizedDarkTheme": "**Solarized Dark 主题风格**",
|
||||
"help.formatting.solirizedLightTheme": "**Solarized Light 主题风格**",
|
||||
"help.formatting.style": "## 文字风格\n\n您可以用 `_` 或 `*` 包围一个词来让它变斜体。用两个则是粗体。\n\n* `_斜体_` 显示为 _斜体_\n* `**粗体**` 显示为 **粗体**\n* `**_粗斜体_**` 显示为 **_粗斜体_**\n* `~~删除线~~` 显示为 ~~删除线~~",
|
||||
"help.formatting.supportedSyntax": "支持的语言:\n`as`, `applescript`, `osascript`, `scpt`, `bash`, `sh`, `zsh`, `clj`, `boot`, `cl2`, `cljc`, `cljs`, `cljs.hl`, `cljscm`, `cljx`, `hic`, `coffee`, `_coffee`, `cake`, `cjsx`, `cson`, `iced`, `cpp`, `c`, `cc`, `h`, `c++`, `h++`, `hpp`, `cs`, `csharp`, `css`, `d`, `di`, `dart`, `delphi`, `dpr`, `dfm`, `pas`, `pascal`, `freepascal`, `lazarus`, `lpr`, `lfm`, `diff`, `django`, `jinja`, `dockerfile`, `docker`, `erl`, `f90`, `f95`, `fsharp`, `fs`, `gcode`, `nc`, `go`, `groovy`, `handlebars`, `hbs`, `html.hbs`, `html.handlebars`, `hs`, `hx`, `java`, `jsp`, `js`, `jsx`, `json`, `jl`, `kt`, `ktm`, `kts`, `less`, `lisp`, `lua`, `mk`, `mak`, `md`, `mkdown`, `mkd`, `matlab`, `m`, `mm`, `objc`, `obj-c`, `ml`, `perl`, `pl`, `php`, `php3`, `php4`, `php5`, `php6`, `ps`, `ps1`, `pp`, `py`, `gyp`, `r`, `ruby`, `rb`, `gemspec`, `podspec`, `thor`, `irb`, `rs`, `scala`, `scm`, `sld`, `scss`, `st`, `sql`, `swift`, `tex`, `vbnet`, `vb`, `bas`, `vbs`, `v`, `veo`, `xml`, `html`, `xhtml`, `rss`, `atom`, `xsl`, `plist`, `yaml`",
|
||||
"help.formatting.supportedSyntax": "支持的语言:\n`as`, `applescript`, `osascript`, `scpt`, `bash`, `sh`, `zsh`, `clj`, `boot`, `cl2`, `cljc`, `cljs`, `cljs.hl`, `cljscm`, `cljx`, `hic`, `coffee`, `_coffee`, `cake`, `cjsx`, `cson`, `iced`, `cpp`, `c`, `cc`, `h`, `c++`, `h++`, `hpp`, `cs`, `csharp`, `css`, `d`, `di`, `dart`, `delphi`, `dpr`, `dfm`, `pas`, `pascal`, `freepascal`, `lazarus`, `lpr`, `lfm`, `diff`, `django`, `jinja`, `dockerfile`, `docker`, `erl`, `f90`, `f95`, `fsharp`, `fs`, `gcode`, `nc`, `go`, `groovy`, `handlebars`, `hbs`, `html.hbs`, `html.handlebars`, `hs`, `hx`, `java`, `jsp`, `js`, `jsx`, `json`, `jl`, `kt`, `ktm`, `kts`, `less`, `lisp`, `lua`, `mk`, `mak`, `md`, `mkdown`, `mkd`, `matlab`, `m`, `mm`, `objc`, `obj-c`, `ml`, `perl`, `pl`, `php`, `php3`, `php4`, `php5`, `php6`, `ps`, `ps1`, `pp`, `py`, `gyp`, `r`, `ruby`, `rb`, `gemspec`, `podspec`, `thor`, `irb`, `rs`, `scala`, `scm`, `sld`, `scss`, `st`, `styl`, `sql`, `swift`, `tex`, `vbnet`, `vb`, `bas`, `vbs`, `v`, `veo`, `xml`, `html`, `xhtml`, `rss`, `atom`, `xsl`, `plist`, `yaml`",
|
||||
"help.formatting.syntax": "### 语法高亮\n\n在代码块开头 ``` 之后输入想高亮的语言以开启高亮。Mattermost 同时提供四种代码风格 (GitHub, Solarized Dark, Solarized Light, Monokai) 可以在 **帐号设定** > **显示** > **主题** > **自定义主题** > **中央频道样式** 里修改",
|
||||
"help.formatting.syntaxEx": " package main\n import \"fmt\"\n func main() {\n fmt.Println(\"Hello, 世界\")\n }",
|
||||
"help.formatting.tableExample": "| 靠左对齐 | 置中对齐 | 靠右对齐 |\n| :------ |:-------:| --------:|\n| 左列 1 | 此文字 | $100 |\n| 左列 2 | 是 | $10 |\n| 左列 3 | 居中的 | $1 |",
|
||||
@@ -1927,18 +1927,18 @@
|
||||
"mobile.notification_settings_mentions.keywordsDescription": "其他触发提及的词",
|
||||
"mobile.notification_settings_mentions.keywordsHelp": "关键字不区分大小写并需要以逗号区分。",
|
||||
"mobile.notification_settings_mentions.wordsTrigger": "触发提及的词",
|
||||
"mobile.notification_settings_mobile.default_sound": "Default ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "Light",
|
||||
"mobile.notification_settings_mobile.default_sound": "默认 ({sound})",
|
||||
"mobile.notification_settings_mobile.light": "淡",
|
||||
"mobile.notification_settings_mobile.no_sound": "无",
|
||||
"mobile.notification_settings_mobile.push_activity": "发送通知",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "发送通知给",
|
||||
"mobile.notification_settings_mobile.push_activity_android": "发送通知",
|
||||
"mobile.notification_settings_mobile.push_status": "触发推送通知当",
|
||||
"mobile.notification_settings_mobile.push_status_android": "触发推送通知当",
|
||||
"mobile.notification_settings_mobile.sound": "Sound",
|
||||
"mobile.notification_settings_mobile.sounds_title": "通知声音",
|
||||
"mobile.notification_settings_mobile.test": "Send me a test notification",
|
||||
"mobile.notification_settings_mobile.test_push": "This is a test push notification",
|
||||
"mobile.notification_settings_mobile.vibrate": "Vibrate",
|
||||
"mobile.notification_settings_mobile.sound": "音效",
|
||||
"mobile.notification_settings_mobile.sounds_title": "通知音效",
|
||||
"mobile.notification_settings_mobile.test": "给我发送个测试通知",
|
||||
"mobile.notification_settings_mobile.test_push": "这是一个推送通知测试",
|
||||
"mobile.notification_settings_mobile.vibrate": "震动",
|
||||
"mobile.offlineIndicator.connected": "已连接",
|
||||
"mobile.offlineIndicator.connecting": "正在连接...",
|
||||
"mobile.offlineIndicator.offline": "无网络连接",
|
||||
@@ -1953,7 +1953,7 @@
|
||||
"mobile.post.retry": "刷新",
|
||||
"mobile.post_info.add_reaction": "添加反应",
|
||||
"mobile.request.invalid_response": "从服务器收到了无效回应。",
|
||||
"mobile.retry_message": "Refreshing messages failed. Pull up to try again.",
|
||||
"mobile.retry_message": "刷新消息失败。拉上以重试。",
|
||||
"mobile.routes.channelInfo": "信息",
|
||||
"mobile.routes.channelInfo.createdBy": "{creator} 创建于 ",
|
||||
"mobile.routes.channelInfo.delete_channel": "删除频道",
|
||||
|
||||
BIN
assets/release/icons/ios/iTunesArtwork@2x.png
Normal file
BIN
assets/release/icons/ios/iTunesArtwork@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 460 KiB |
@@ -423,6 +423,12 @@ platform :android do
|
||||
new_string: 'package com.mattermost.rn;'
|
||||
)
|
||||
|
||||
find_replace_string(
|
||||
path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationPreferences.java',
|
||||
old_string: 'package com.mattermost.rnbeta;',
|
||||
new_string: 'package com.mattermost.rn;'
|
||||
)
|
||||
|
||||
find_replace_string(
|
||||
path_to_file: './android/app/src/main/java/com/mattermost/rn/MattermostManagedModule.java',
|
||||
old_string: 'package com.mattermost.rnbeta;',
|
||||
|
||||
@@ -24,7 +24,7 @@ GEM
|
||||
faraday_middleware (0.12.2)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.0)
|
||||
fastlane (2.58.0)
|
||||
fastlane (2.59.0)
|
||||
CFPropertyList (>= 2.3, < 3.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
@@ -126,7 +126,7 @@ GEM
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.3.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.5.1)
|
||||
xcodeproj (1.5.2)
|
||||
CFPropertyList (~> 2.3.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
|
||||
@@ -1954,7 +1954,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 57;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1977,6 +1977,7 @@
|
||||
"$(SRCROOT)/../node_modules/react-native-youtube/**",
|
||||
);
|
||||
INFOPLIST_FILE = Mattermost/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -2000,7 +2001,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 50;
|
||||
CURRENT_PROJECT_VERSION = 57;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2023,6 +2024,7 @@
|
||||
"$(SRCROOT)/../node_modules/react-native-youtube/**",
|
||||
);
|
||||
INFOPLIST_FILE = Mattermost/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
"idiom" : "ipad",
|
||||
"filename" : "83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "iTunesArtwork@2x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 863 KiB |
@@ -32,7 +32,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>50</string>
|
||||
<string>57</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
@@ -92,6 +92,7 @@
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>50</string>
|
||||
<string>57</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"analytics-react-native": "1.1.0",
|
||||
"babel-polyfill": "6.23.0",
|
||||
"commonmark": "hmhealey/commonmark.js#25dc6a4c456db579631797e29847752cb5e7d8d7",
|
||||
"commonmark-react-renderer": "hmhealey/commonmark-react-renderer#c5d00343664c89da40d5a2ffa8b083e7cc1615d7",
|
||||
"commonmark-react-renderer": "hmhealey/commonmark-react-renderer#6e259b66ae87d31d2f908effcd05776b9ea3446f",
|
||||
"deep-equal": "1.0.1",
|
||||
"intl": "1.2.5",
|
||||
"jail-monkey": "0.0.9",
|
||||
@@ -88,7 +88,8 @@
|
||||
"react-native-svg-mock": "1.0.2",
|
||||
"react-test-renderer": "16.0.0-alpha.12",
|
||||
"remote-redux-devtools": "0.5.12",
|
||||
"remote-redux-devtools-on-debugger": "0.8.2"
|
||||
"remote-redux-devtools-on-debugger": "0.8.2",
|
||||
"underscore": "1.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test nyc --reporter=text mocha --opts test/mocha.opts",
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -1623,9 +1623,9 @@ commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
|
||||
commonmark-react-renderer@hmhealey/commonmark-react-renderer#c5d00343664c89da40d5a2ffa8b083e7cc1615d7:
|
||||
commonmark-react-renderer@hmhealey/commonmark-react-renderer#6e259b66ae87d31d2f908effcd05776b9ea3446f:
|
||||
version "4.3.3"
|
||||
resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/c5d00343664c89da40d5a2ffa8b083e7cc1615d7"
|
||||
resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/6e259b66ae87d31d2f908effcd05776b9ea3446f"
|
||||
dependencies:
|
||||
in-publish "^2.0.0"
|
||||
lodash.assign "^4.2.0"
|
||||
@@ -3878,7 +3878,7 @@ makeerror@1.0.x:
|
||||
|
||||
mattermost-redux@mattermost/mattermost-redux#master:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/850e7df06cd7dcae7e592f78ca7bbdd6ab2ec56a"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/85db14af9a6d8c402213b0eb3c372f1ac6adad34"
|
||||
dependencies:
|
||||
deep-equal "1.0.1"
|
||||
harmony-reflect "1.5.1"
|
||||
@@ -6411,6 +6411,10 @@ ultron@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
|
||||
|
||||
underscore@1.8.3:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
||||
Reference in New Issue
Block a user