forked from Ivasoft/mattermost-mobile
Compare commits
15 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13b36d1eab | ||
|
|
51e3c4c8b4 | ||
|
|
2abe5cb8b1 | ||
|
|
49be62695e | ||
|
|
d7d140b1b4 | ||
|
|
d4259e7f16 | ||
|
|
0d94f74237 | ||
|
|
483f7187fc | ||
|
|
a5297c328c | ||
|
|
3f9a941aa1 | ||
|
|
a60d3eeb92 | ||
|
|
562d3ac9ad | ||
|
|
42cee12c19 | ||
|
|
df00b86d26 | ||
|
|
a11aad3a5a |
@@ -132,8 +132,8 @@ android {
|
|||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||||
versionCode 354
|
versionCode 356
|
||||||
versionName "1.42.0"
|
versionName "1.43.0"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.json.JSONException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
@@ -45,6 +46,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
super(reactContext);
|
super(reactContext);
|
||||||
mApplication = application;
|
mApplication = application;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File tempFolder;
|
private File tempFolder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -131,6 +133,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
String text = "";
|
String text = "";
|
||||||
String type = "";
|
String type = "";
|
||||||
String action = "";
|
String action = "";
|
||||||
|
String extra = "";
|
||||||
|
|
||||||
Activity currentActivity = getCurrentActivity();
|
Activity currentActivity = getCurrentActivity();
|
||||||
|
|
||||||
@@ -139,20 +142,21 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
Intent intent = currentActivity.getIntent();
|
Intent intent = currentActivity.getIntent();
|
||||||
action = intent.getAction();
|
action = intent.getAction();
|
||||||
type = intent.getType();
|
type = intent.getType();
|
||||||
|
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
type = "";
|
type = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
|
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
|
||||||
text = intent.getStringExtra(Intent.EXTRA_TEXT);
|
map.putString("value", extra);
|
||||||
map.putString("value", text);
|
|
||||||
map.putString("type", type);
|
map.putString("type", type);
|
||||||
|
map.putBoolean("isString", true);
|
||||||
items.pushMap(map);
|
items.pushMap(map);
|
||||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||||
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
|
||||||
map.putString("value", text);
|
|
||||||
|
|
||||||
if (type.equals("image/*")) {
|
if (type.equals("image/*")) {
|
||||||
type = "image/jpeg";
|
type = "image/jpeg";
|
||||||
@@ -161,17 +165,16 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
map.putString("type", type);
|
map.putString("type", type);
|
||||||
|
map.putBoolean("isString", false);
|
||||||
items.pushMap(map);
|
items.pushMap(map);
|
||||||
}
|
}
|
||||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||||
for (Uri uri : uris) {
|
for (Uri uri : Objects.requireNonNull(uris)) {
|
||||||
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
|
||||||
map = Arguments.createMap();
|
map = Arguments.createMap();
|
||||||
text = "file://" + filePath;
|
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
|
||||||
map.putString("value", text);
|
|
||||||
|
|
||||||
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
|
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
|
||||||
|
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
if (type.equals("image/*")) {
|
if (type.equals("image/*")) {
|
||||||
type = "image/jpeg";
|
type = "image/jpeg";
|
||||||
@@ -182,6 +185,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
type = "application/octet-stream";
|
type = "application/octet-stream";
|
||||||
}
|
}
|
||||||
map.putString("type", type);
|
map.putString("type", type);
|
||||||
|
map.putBoolean("isString", false);
|
||||||
items.pushMap(map);
|
items.pushMap(map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +225,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM);
|
.setType(MultipartBody.FORM);
|
||||||
|
|
||||||
for(int i = 0 ; i < files.size() ; i++) {
|
for (int i = 0; i < files.size(); i++) {
|
||||||
ReadableMap file = files.getMap(i);
|
ReadableMap file = files.getMap(i);
|
||||||
String filePath = file.getString("fullPath").replaceFirst("file://", "");
|
String filePath = file.getString("fullPath").replaceFirst("file://", "");
|
||||||
File fileInfo = new File(filePath);
|
File fileInfo = new File(filePath);
|
||||||
@@ -245,7 +249,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
|||||||
JSONObject responseJson = new JSONObject(responseData);
|
JSONObject responseJson = new JSONObject(responseData);
|
||||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||||
JSONArray file_ids = new JSONArray();
|
JSONArray file_ids = new JSONArray();
|
||||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
for (int i = 0; i < fileInfoArray.length(); i++) {
|
||||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||||
file_ids.put(fileInfo.getString("id"));
|
file_ids.put(fileInfo.getString("id"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Client4} from '@client/rest';
|
import {Client4} from '@client/rest';
|
||||||
import {ActionFunc} from '@mm-redux/types/actions';
|
|
||||||
import {AppCallResponse, AppForm, AppCallRequest, AppCallType} from '@mm-redux/types/apps';
|
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
|
||||||
|
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||||
|
import {Post} from '@mm-redux/types/posts';
|
||||||
|
|
||||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||||
import {showModal} from './navigation';
|
import {showModal} from './navigation';
|
||||||
@@ -12,6 +15,8 @@ import CompassIcon from '@components/compass_icon';
|
|||||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import EphemeralStore from '@store/ephemeral_store';
|
||||||
import {makeCallErrorResponse} from '@utils/apps';
|
import {makeCallErrorResponse} from '@utils/apps';
|
||||||
|
import {sendEphemeralPost} from '@actions/views/post';
|
||||||
|
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||||
|
|
||||||
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
|
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
@@ -114,3 +119,47 @@ const showAppForm = async (form: AppForm, call: AppCallRequest, theme: Theme) =>
|
|||||||
const passProps = {form, call};
|
const passProps = {form, call};
|
||||||
showModal('AppForm', form.title, passProps, options);
|
showModal('AppForm', form.title, passProps, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
|
||||||
|
return (dispatch: DispatchFunc) => {
|
||||||
|
return dispatch(sendEphemeralPost(
|
||||||
|
message,
|
||||||
|
post.channel_id,
|
||||||
|
post.root_id || post.id,
|
||||||
|
response.app_metadata?.bot_user_id,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postEphemeralCallResponseForChannel(response: AppCallResponse, message: string, channelID: string): ActionFunc {
|
||||||
|
return (dispatch: DispatchFunc) => {
|
||||||
|
return dispatch(sendEphemeralPost(
|
||||||
|
message,
|
||||||
|
channelID,
|
||||||
|
'',
|
||||||
|
response.app_metadata?.bot_user_id,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postEphemeralCallResponseForContext(response: AppCallResponse, message: string, context: AppContext): ActionFunc {
|
||||||
|
return (dispatch: DispatchFunc) => {
|
||||||
|
return dispatch(sendEphemeralPost(
|
||||||
|
message,
|
||||||
|
context.channel_id,
|
||||||
|
context.root_id || context.post_id,
|
||||||
|
response.app_metadata?.bot_user_id,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postEphemeralCallResponseForCommandArgs(response: AppCallResponse, message: string, args: CommandArgs): ActionFunc {
|
||||||
|
return (dispatch: DispatchFunc) => {
|
||||||
|
return dispatch(sendEphemeralPost(
|
||||||
|
message,
|
||||||
|
args.channel_id,
|
||||||
|
args.root_id,
|
||||||
|
response.app_metadata?.bot_user_id,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ function setLoadMorePostsVisible(visible) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGroupData() {
|
function loadGroupData(isReconnect = false) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const actions = [];
|
const actions = [];
|
||||||
@@ -665,9 +665,10 @@ function loadGroupData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const getGroupsSince = isReconnect ? (state.websocket?.lastDisconnectAt || 0) : undefined;
|
||||||
const [getAllGroupsAssociatedToChannelsInTeam, getGroups] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
const [getAllGroupsAssociatedToChannelsInTeam, getGroups] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||||
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
|
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
|
||||||
Client4.getGroups(true, 0, 0),
|
Client4.getGroups(false, 0, 0, getGroupsSince),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
|
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
|
||||||
@@ -713,7 +714,7 @@ function loadGroupData() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect = false) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const currentUserId = getCurrentUserId(state);
|
const currentUserId = getCurrentUserId(state);
|
||||||
@@ -784,7 +785,7 @@ export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
|||||||
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
|
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(loadGroupData());
|
dispatch(loadGroupData(isReconnect));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {data};
|
return {data};
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import {executeCommand as executeCommandService} from '@mm-redux/actions/integra
|
|||||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||||
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
||||||
|
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||||
|
|
||||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||||
|
|
||||||
import {doAppCall} from '@actions/apps';
|
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||||
import {appsEnabled} from '@utils/apps';
|
import {appsEnabled} from '@utils/apps';
|
||||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||||
import {sendEphemeralPost} from './post';
|
|
||||||
|
|
||||||
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
|
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
@@ -22,7 +22,7 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
|||||||
|
|
||||||
const teamId = getCurrentTeamId(state);
|
const teamId = getCurrentTeamId(state);
|
||||||
|
|
||||||
const args = {
|
const args: CommandArgs = {
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
team_id: teamId,
|
team_id: teamId,
|
||||||
root_id: rootId,
|
root_id: rootId,
|
||||||
@@ -65,7 +65,7 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
|||||||
switch (callResp.type) {
|
switch (callResp.type) {
|
||||||
case AppCallResponseTypes.OK:
|
case AppCallResponseTypes.OK:
|
||||||
if (callResp.markdown) {
|
if (callResp.markdown) {
|
||||||
dispatch(sendEphemeralPost(callResp.markdown, args.channel_id, args.parent_id, callResp.app_metadata?.bot_user_id));
|
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
|
||||||
}
|
}
|
||||||
return {data: {}};
|
return {data: {}};
|
||||||
case AppCallResponseTypes.FORM:
|
case AppCallResponseTypes.FORM:
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function doReconnect(now: number) {
|
|||||||
const currentTeamMembership = me.teamMembers.find((tm: TeamMembership) => tm.team_id === currentTeamId && tm.delete_at === 0);
|
const currentTeamMembership = me.teamMembers.find((tm: TeamMembership) => tm.team_id === currentTeamId && tm.delete_at === 0);
|
||||||
|
|
||||||
if (currentTeamMembership) {
|
if (currentTeamMembership) {
|
||||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true, true));
|
||||||
|
|
||||||
if (myData?.channels && myData?.channelMembers) {
|
if (myData?.channels && myData?.channelMembers) {
|
||||||
actions.push({
|
actions.push({
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
||||||
|
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||||
|
|
||||||
export interface ClientAppsMix {
|
export interface ClientAppsMix {
|
||||||
executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise<AppCallResponse>;
|
executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise<AppCallResponse>;
|
||||||
getAppsBindings: (userID: string, channelID: string) => Promise<AppBinding[]>;
|
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientApps = (superclass: any) => class extends superclass {
|
const ClientApps = (superclass: any) => class extends superclass {
|
||||||
@@ -25,9 +26,16 @@ const ClientApps = (superclass: any) => class extends superclass {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppsBindings = async (userID: string, channelID: string) => {
|
getAppsBindings = async (userID: string, channelID: string, teamID: string) => {
|
||||||
|
const params = {
|
||||||
|
user_id: userID,
|
||||||
|
channel_id: channelID,
|
||||||
|
team_id: teamID,
|
||||||
|
user_agent: 'mobile',
|
||||||
|
};
|
||||||
|
|
||||||
return this.doFetch(
|
return this.doFetch(
|
||||||
this.getAppsProxyRoute() + `/api/v1/bindings?user_id=${userID}&channel_id=${channelID}&user_agent_type=mobile`,
|
`${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ export interface ClientGroupsMix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ClientGroups = (superclass: any) => class extends superclass {
|
const ClientGroups = (superclass: any) => class extends superclass {
|
||||||
getGroups = async (filterAllowReference = false, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
getGroups = async (filterAllowReference = false, page = 0, perPage = PER_PAGE_DEFAULT, since = 0) => {
|
||||||
return this.doFetch(
|
return this.doFetch(
|
||||||
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage})}`,
|
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage, since})}`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"backgroundColor": "#ffe577",
|
"backgroundColor": "#ffe577",
|
||||||
"color": "#145dbf",
|
"color": "#166de0",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export default class AtMention extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
mentionTextStyle.push({backgroundColor, color: theme.mentionColor});
|
mentionTextStyle.push({backgroundColor, color: theme.mentionHighlightLink});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export {getUserByUsername as selectUserByUsername} from '@mm-redux/selectors/ent
|
|||||||
|
|
||||||
export {getUserByUsername} from '@mm-redux/actions/users';
|
export {getUserByUsername} from '@mm-redux/actions/users';
|
||||||
export {getChannelByNameAndTeamName} from '@mm-redux/actions/channels';
|
export {getChannelByNameAndTeamName} from '@mm-redux/actions/channels';
|
||||||
export {sendEphemeralPost} from '@actions/views/post';
|
|
||||||
|
|
||||||
export {doAppCall} from '@actions/apps';
|
export {doAppCall} from '@actions/apps';
|
||||||
export {createCallRequest} from '@utils/apps';
|
export {createCallRequest} from '@utils/apps';
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from 'react-native';
|
|
||||||
|
|
||||||
import {General} from '@mm-redux/constants';
|
|
||||||
|
|
||||||
import CompassIcon from '@components/compass_icon';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
||||||
|
|
||||||
export default class ChannelIcon extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
isActive: PropTypes.bool,
|
|
||||||
isInfo: PropTypes.bool,
|
|
||||||
isUnread: PropTypes.bool,
|
|
||||||
hasDraft: PropTypes.bool,
|
|
||||||
membersCount: PropTypes.number,
|
|
||||||
size: PropTypes.number,
|
|
||||||
status: PropTypes.string,
|
|
||||||
theme: PropTypes.object.isRequired,
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
isArchived: PropTypes.bool.isRequired,
|
|
||||||
isBot: PropTypes.bool.isRequired,
|
|
||||||
testID: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
isActive: false,
|
|
||||||
isInfo: false,
|
|
||||||
isUnread: false,
|
|
||||||
size: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isActive,
|
|
||||||
isUnread,
|
|
||||||
isInfo,
|
|
||||||
hasDraft,
|
|
||||||
membersCount,
|
|
||||||
size,
|
|
||||||
status,
|
|
||||||
theme,
|
|
||||||
type,
|
|
||||||
isArchived,
|
|
||||||
isBot,
|
|
||||||
testID,
|
|
||||||
} = this.props;
|
|
||||||
const style = getStyleSheet(theme);
|
|
||||||
|
|
||||||
let activeIcon;
|
|
||||||
let unreadIcon;
|
|
||||||
let activeGroupBox;
|
|
||||||
let unreadGroupBox;
|
|
||||||
let activeGroup;
|
|
||||||
let unreadGroup;
|
|
||||||
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
|
|
||||||
|
|
||||||
if (isUnread) {
|
|
||||||
unreadIcon = style.iconUnread;
|
|
||||||
unreadGroupBox = style.groupBoxUnread;
|
|
||||||
unreadGroup = style.groupUnread;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
activeIcon = style.iconActive;
|
|
||||||
activeGroupBox = style.groupBoxActive;
|
|
||||||
activeGroup = style.groupActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInfo) {
|
|
||||||
activeIcon = style.iconInfo;
|
|
||||||
activeGroupBox = style.groupBoxInfo;
|
|
||||||
activeGroup = style.groupInfo;
|
|
||||||
offlineColor = changeOpacity(theme.centerChannelColor, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon;
|
|
||||||
if (isArchived) {
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='archive-outline'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
|
||||||
testID={`${testID}.archive`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (isBot) {
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='robot-happy'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, left: -1.5, top: -1}]}
|
|
||||||
testID={`${testID}.bot`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (hasDraft) {
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='pencil-outline'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
|
||||||
testID={`${testID}.draft`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (type === General.OPEN_CHANNEL) {
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='globe'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
|
||||||
testID={`${testID}.public`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (type === General.PRIVATE_CHANNEL) {
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='lock-outline'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
|
|
||||||
testID={`${testID}.private`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (type === General.GM_CHANNEL) {
|
|
||||||
const fontSize = (size - 10);
|
|
||||||
icon = (
|
|
||||||
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}>
|
|
||||||
<Text
|
|
||||||
style={[style.group, unreadGroup, activeGroup, {fontSize}]}
|
|
||||||
testID={`${testID}.gm_member_count`}
|
|
||||||
>
|
|
||||||
{membersCount}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
} else if (type === General.DM_CHANNEL) {
|
|
||||||
switch (status) {
|
|
||||||
case General.AWAY:
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='clock'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.awayIndicator}]}
|
|
||||||
testID={`${testID}.away`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case General.DND:
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='minus-circle'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.dndIndicator}]}
|
|
||||||
testID={`${testID}.dnd`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case General.ONLINE:
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='check-circle'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.onlineIndicator}]}
|
|
||||||
testID={`${testID}.online`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='circle-outline'
|
|
||||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: offlineColor}]}
|
|
||||||
testID={`${testID}.offline`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[style.container, {height: size}]}>
|
|
||||||
{icon}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|
||||||
return {
|
|
||||||
container: {
|
|
||||||
marginRight: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
color: changeOpacity(theme.sidebarText, 0.4),
|
|
||||||
},
|
|
||||||
iconActive: {
|
|
||||||
color: theme.sidebarTextActiveColor,
|
|
||||||
},
|
|
||||||
iconUnread: {
|
|
||||||
color: theme.sidebarUnreadText,
|
|
||||||
},
|
|
||||||
iconInfo: {
|
|
||||||
color: theme.centerChannelColor,
|
|
||||||
},
|
|
||||||
groupBox: {
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: changeOpacity(theme.sidebarText, 0.3),
|
|
||||||
borderColor: changeOpacity(theme.sidebarText, 0.3),
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 2,
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
groupBoxActive: {
|
|
||||||
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3),
|
|
||||||
},
|
|
||||||
groupBoxUnread: {
|
|
||||||
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
|
|
||||||
},
|
|
||||||
groupBoxInfo: {
|
|
||||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
color: changeOpacity(theme.sidebarText, 0.6),
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
groupActive: {
|
|
||||||
color: theme.sidebarTextActiveColor,
|
|
||||||
},
|
|
||||||
groupUnread: {
|
|
||||||
color: theme.sidebarUnreadText,
|
|
||||||
},
|
|
||||||
groupInfo: {
|
|
||||||
color: theme.centerChannelColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
184
app/components/channel_icon.tsx
Normal file
184
app/components/channel_icon.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import ProfilePicture from '@components/profile_picture';
|
||||||
|
import {General} from '@mm-redux/constants';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import type {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hasDraft: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
isArchived: boolean;
|
||||||
|
isInfo: boolean;
|
||||||
|
isUnread: boolean;
|
||||||
|
membersCount: number;
|
||||||
|
size: number;
|
||||||
|
statusStyle?: StyleProp<ViewStyle>;
|
||||||
|
testID?: string;
|
||||||
|
theme: Theme;
|
||||||
|
type: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: changeOpacity(theme.sidebarText, 0.4),
|
||||||
|
},
|
||||||
|
iconActive: {
|
||||||
|
color: theme.sidebarTextActiveColor,
|
||||||
|
},
|
||||||
|
iconUnread: {
|
||||||
|
color: theme.sidebarUnreadText,
|
||||||
|
},
|
||||||
|
iconInfo: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
},
|
||||||
|
groupBox: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: changeOpacity(theme.sidebarText, 0.16),
|
||||||
|
borderRadius: 4,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
groupBoxActive: {
|
||||||
|
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3),
|
||||||
|
},
|
||||||
|
groupBoxUnread: {
|
||||||
|
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
|
||||||
|
},
|
||||||
|
groupBoxInfo: {
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
color: theme.sidebarText,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
groupActive: {
|
||||||
|
color: theme.sidebarTextActiveColor,
|
||||||
|
},
|
||||||
|
groupUnread: {
|
||||||
|
color: theme.sidebarUnreadText,
|
||||||
|
},
|
||||||
|
groupInfo: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChannelIcon = (props: Props) => {
|
||||||
|
const style = getStyleSheet(props.theme);
|
||||||
|
|
||||||
|
let activeIcon;
|
||||||
|
let unreadIcon;
|
||||||
|
let activeGroupBox;
|
||||||
|
let unreadGroupBox;
|
||||||
|
let activeGroup;
|
||||||
|
let unreadGroup;
|
||||||
|
|
||||||
|
if (props.isUnread) {
|
||||||
|
unreadIcon = style.iconUnread;
|
||||||
|
unreadGroupBox = style.groupBoxUnread;
|
||||||
|
unreadGroup = style.groupUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isActive) {
|
||||||
|
activeIcon = style.iconActive;
|
||||||
|
activeGroupBox = style.groupBoxActive;
|
||||||
|
activeGroup = style.groupActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isInfo) {
|
||||||
|
activeIcon = style.iconInfo;
|
||||||
|
activeGroupBox = style.groupBoxInfo;
|
||||||
|
activeGroup = style.groupInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
let extraStyle;
|
||||||
|
if (props.isArchived) {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='archive-outline'
|
||||||
|
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 1}]}
|
||||||
|
testID={`${props.testID}.archive`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (props.hasDraft) {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='pencil-outline'
|
||||||
|
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 2}]}
|
||||||
|
testID={`${props.testID}.draft`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (props.type === General.OPEN_CHANNEL) {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='globe'
|
||||||
|
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 1}]}
|
||||||
|
testID={`${props.testID}.public`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (props.type === General.PRIVATE_CHANNEL) {
|
||||||
|
icon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='lock-outline'
|
||||||
|
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 0.5}]}
|
||||||
|
testID={`${props.testID}.private`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (props.type === General.GM_CHANNEL) {
|
||||||
|
const fontSize = (props.size - 12);
|
||||||
|
const boxSize = (props.size - 4);
|
||||||
|
icon = (
|
||||||
|
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: boxSize, height: boxSize}]}>
|
||||||
|
<Text
|
||||||
|
style={[style.group, unreadGroup, activeGroup, {fontSize}]}
|
||||||
|
testID={`${props.testID}.gm_member_count`}
|
||||||
|
>
|
||||||
|
{props.membersCount}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else if (props.type === General.DM_CHANNEL) {
|
||||||
|
// extraStyle = {marginRight: 6};
|
||||||
|
icon = (
|
||||||
|
<ProfilePicture
|
||||||
|
size={props.size}
|
||||||
|
statusSize={12}
|
||||||
|
userId={props.userId}
|
||||||
|
testID={props.testID}
|
||||||
|
statusStyle={props.statusStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[style.container, extraStyle, {width: props.size, height: props.size}]}>
|
||||||
|
{icon}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChannelIcon.defaultProps = {
|
||||||
|
hasDraft: false,
|
||||||
|
isActive: false,
|
||||||
|
isArchived: false,
|
||||||
|
isInfo: false,
|
||||||
|
isUnread: false,
|
||||||
|
membersCount: 0,
|
||||||
|
size: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelIcon;
|
||||||
@@ -11,18 +11,18 @@ import {getStatusColors} from '@utils/message_attachment_colors';
|
|||||||
import ButtonBindingText from './button_binding_text';
|
import ButtonBindingText from './button_binding_text';
|
||||||
import {Theme} from '@mm-redux/types/preferences';
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
import {ActionResult} from '@mm-redux/types/actions';
|
import {ActionResult} from '@mm-redux/types/actions';
|
||||||
import {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {AppBinding} from '@mm-redux/types/apps';
|
||||||
import {Post} from '@mm-redux/types/posts';
|
import {Post} from '@mm-redux/types/posts';
|
||||||
|
import {DoAppCall, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||||
import {AppExpandLevels, AppBindingLocations, AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
import {AppExpandLevels, AppBindingLocations, AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||||
import {Channel} from '@mm-redux/types/channels';
|
import {Channel} from '@mm-redux/types/channels';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
getChannel: (channelId: string) => Promise<ActionResult>;
|
getChannel: (channelId: string) => Promise<ActionResult>;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||||
};
|
};
|
||||||
post: Post;
|
post: Post;
|
||||||
binding: AppBinding;
|
binding: AppBinding;
|
||||||
@@ -72,14 +72,13 @@ export default class ButtonBinding extends PureComponent<Props> {
|
|||||||
this.setState({executing: false});
|
this.setState({executing: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ephemeral = (message: string) => this.props.actions.sendEphemeralPost(message, this.props.post.channel_id, this.props.post.root_id, res.data?.app_metadata?.bot_user_id);
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
const errorResponse = res.error;
|
const errorResponse = res.error;
|
||||||
const errorMessage = errorResponse.error || intl.formatMessage(
|
const errorMessage = errorResponse.error || intl.formatMessage(
|
||||||
{id: 'apps.error.unknown',
|
{id: 'apps.error.unknown',
|
||||||
defaultMessage: 'Unknown error occurred.',
|
defaultMessage: 'Unknown error occurred.',
|
||||||
});
|
});
|
||||||
ephemeral(errorMessage);
|
this.props.actions.postEphemeralCallResponseForPost(errorResponse, errorMessage, post);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ export default class ButtonBinding extends PureComponent<Props> {
|
|||||||
switch (callResp.type) {
|
switch (callResp.type) {
|
||||||
case AppCallResponseTypes.OK:
|
case AppCallResponseTypes.OK:
|
||||||
if (callResp.markdown) {
|
if (callResp.markdown) {
|
||||||
ephemeral(callResp.markdown);
|
this.props.actions.postEphemeralCallResponseForPost(callResp, callResp.markdown, post);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case AppCallResponseTypes.NAVIGATE:
|
case AppCallResponseTypes.NAVIGATE:
|
||||||
@@ -104,7 +103,7 @@ export default class ButtonBinding extends PureComponent<Props> {
|
|||||||
type: callResp.type,
|
type: callResp.type,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ephemeral(errorMessage);
|
this.props.actions.postEphemeralCallResponseForPost(callResp, errorMessage, post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {reEmoji, reEmoticon, reMain} from '@constants/emoji';
|
|||||||
import {getEmoticonName} from '@utils/emoji_utils';
|
import {getEmoticonName} from '@utils/emoji_utils';
|
||||||
|
|
||||||
export default function ButtonBindingText({message, style}: {message: string; style: StyleProp<any>}) {
|
export default function ButtonBindingText({message, style}: {message: string; style: StyleProp<any>}) {
|
||||||
const components = [] as JSX.Element[];
|
const components = [] as React.ReactNode[];
|
||||||
|
|
||||||
let text = message;
|
let text = message;
|
||||||
while (text) {
|
while (text) {
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
|||||||
|
|
||||||
import {GlobalState} from '@mm-redux/types/store';
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
import {ActionFunc, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
import {ActionFunc, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||||
import {AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {DoAppCall, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||||
import {doAppCall} from '@actions/apps';
|
import {doAppCall, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||||
import {getPost} from '@mm-redux/selectors/entities/posts';
|
import {getPost} from '@mm-redux/selectors/entities/posts';
|
||||||
|
|
||||||
import ButtonBinding from './button_binding';
|
import ButtonBinding from './button_binding';
|
||||||
import {getChannel} from '@mm-redux/actions/channels';
|
import {getChannel} from '@mm-redux/actions/channels';
|
||||||
import {sendEphemeralPost} from '@actions/views/post';
|
|
||||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
postId: string;
|
postId: string;
|
||||||
@@ -31,9 +29,9 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
getChannel: (channelId: string) => Promise<ActionResult>;
|
getChannel: (channelId: string) => Promise<ActionResult>;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||||
@@ -41,7 +39,7 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||||
doAppCall,
|
doAppCall,
|
||||||
getChannel,
|
getChannel,
|
||||||
sendEphemeralPost,
|
postEphemeralCallResponseForPost,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function EmbeddedSubBindings(props: Props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = [] as JSX.Element[];
|
const content = [] as React.ReactNode[];
|
||||||
|
|
||||||
bindings.forEach((binding) => {
|
bindings.forEach((binding) => {
|
||||||
if (!binding.app_id || !binding.call) {
|
if (!binding.app_id || !binding.call) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function EmbeddedBindings(props: Props) {
|
|||||||
theme,
|
theme,
|
||||||
textStyles,
|
textStyles,
|
||||||
} = props;
|
} = props;
|
||||||
const content = [] as JSX.Element[];
|
const content = [] as React.ReactNode[];
|
||||||
|
|
||||||
embeds.forEach((embed, i) => {
|
embeds.forEach((embed, i) => {
|
||||||
content.push(
|
content.push(
|
||||||
|
|||||||
@@ -5,16 +5,14 @@ import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
|||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
import {GlobalState} from '@mm-redux/types/store';
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
import {doAppCall} from '@actions/apps';
|
|
||||||
import {ActionFunc, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
import {ActionFunc, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||||
import {AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {DoAppCall, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
|
||||||
import {getPost} from '@mm-redux/selectors/entities/posts';
|
import {getPost} from '@mm-redux/selectors/entities/posts';
|
||||||
|
|
||||||
import MenuBinding from './menu_binding';
|
import MenuBinding from './menu_binding';
|
||||||
import {getChannel} from '@mm-redux/actions/channels';
|
import {getChannel} from '@mm-redux/actions/channels';
|
||||||
import {sendEphemeralPost} from '@actions/views/post';
|
|
||||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||||
|
import {doAppCall, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
postId: string;
|
postId: string;
|
||||||
@@ -28,9 +26,9 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
getChannel: (channelId: string) => Promise<ActionResult>;
|
getChannel: (channelId: string) => Promise<ActionResult>;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||||
@@ -38,7 +36,7 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||||
doAppCall,
|
doAppCall,
|
||||||
getChannel,
|
getChannel,
|
||||||
sendEphemeralPost,
|
postEphemeralCallResponseForPost,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ import AutocompleteSelector from 'app/components/autocomplete_selector';
|
|||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
import {PostActionOption} from '@mm-redux/types/integration_actions';
|
import {PostActionOption} from '@mm-redux/types/integration_actions';
|
||||||
import {Post} from '@mm-redux/types/posts';
|
import {Post} from '@mm-redux/types/posts';
|
||||||
import {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {AppBinding} from '@mm-redux/types/apps';
|
||||||
import {ActionResult} from '@mm-redux/types/actions';
|
import {ActionResult} from '@mm-redux/types/actions';
|
||||||
|
import {DoAppCall, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||||
import {AppExpandLevels, AppBindingLocations, AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
import {AppExpandLevels, AppBindingLocations, AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||||
import {Channel} from '@mm-redux/types/channels';
|
import {Channel} from '@mm-redux/types/channels';
|
||||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
getChannel: (channelId: string) => Promise<ActionResult>;
|
getChannel: (channelId: string) => Promise<ActionResult>;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||||
};
|
};
|
||||||
binding?: AppBinding;
|
binding?: AppBinding;
|
||||||
post: Post;
|
post: Post;
|
||||||
@@ -82,16 +82,14 @@ export default class MenuBinding extends PureComponent<Props, State> {
|
|||||||
{post: AppExpandLevels.EXPAND_ALL},
|
{post: AppExpandLevels.EXPAND_ALL},
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await actions.doAppCall(call, AppCallTypes.SUBMIT, this.context.intl);
|
const res = await actions.doAppCall(call, AppCallTypes.SUBMIT, intl);
|
||||||
|
|
||||||
const ephemeral = (message: string) => this.props.actions.sendEphemeralPost(message, this.props.post.channel_id, this.props.post.root_id, res.data?.app_metadata?.bot_user_id);
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
const errorResponse = res.error;
|
const errorResponse = res.error;
|
||||||
const errorMessage = errorResponse.error || intl.formatMessage({
|
const errorMessage = errorResponse.error || intl.formatMessage({
|
||||||
id: 'apps.error.unknown',
|
id: 'apps.error.unknown',
|
||||||
defaultMessage: 'Unknown error occurred.',
|
defaultMessage: 'Unknown error occurred.',
|
||||||
});
|
});
|
||||||
ephemeral(errorMessage);
|
this.props.actions.postEphemeralCallResponseForPost(res.error, errorMessage, post);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ export default class MenuBinding extends PureComponent<Props, State> {
|
|||||||
switch (callResp.type) {
|
switch (callResp.type) {
|
||||||
case AppCallResponseTypes.OK:
|
case AppCallResponseTypes.OK:
|
||||||
if (callResp.markdown) {
|
if (callResp.markdown) {
|
||||||
ephemeral(callResp.markdown);
|
this.props.actions.postEphemeralCallResponseForPost(callResp, callResp.markdown, post);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case AppCallResponseTypes.NAVIGATE:
|
case AppCallResponseTypes.NAVIGATE:
|
||||||
@@ -112,7 +110,7 @@ export default class MenuBinding extends PureComponent<Props, State> {
|
|||||||
}, {
|
}, {
|
||||||
type: callResp.type,
|
type: callResp.type,
|
||||||
});
|
});
|
||||||
ephemeral(errorMessage);
|
this.props.actions.postEphemeralCallResponseForPost(callResp, errorMessage, post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
|
||||||
Platform,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
} from 'react-native';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
|
|
||||||
export default class Emoji extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Emoji text name.
|
|
||||||
*/
|
|
||||||
emojiName: PropTypes.string.isRequired,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Image URL for the emoji.
|
|
||||||
*/
|
|
||||||
imageUrl: PropTypes.string.isRequired,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set if this is a custom emoji.
|
|
||||||
*/
|
|
||||||
isCustomEmoji: PropTypes.bool.isRequired,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set to render only the text and no image.
|
|
||||||
*/
|
|
||||||
displayTextOnly: PropTypes.bool,
|
|
||||||
literal: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
||||||
unicode: PropTypes.string,
|
|
||||||
customEmojiStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
||||||
testID: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
customEmojis: new Map(),
|
|
||||||
literal: '',
|
|
||||||
imageUrl: '',
|
|
||||||
isCustomEmoji: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
customEmojiStyle,
|
|
||||||
displayTextOnly,
|
|
||||||
imageUrl,
|
|
||||||
literal,
|
|
||||||
unicode,
|
|
||||||
testID,
|
|
||||||
textStyle,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let size = this.props.size;
|
|
||||||
let fontSize = size;
|
|
||||||
if (!size && textStyle) {
|
|
||||||
const flatten = StyleSheet.flatten(textStyle);
|
|
||||||
fontSize = flatten.fontSize;
|
|
||||||
size = fontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayTextOnly) {
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
style={textStyle}
|
|
||||||
testID={testID}
|
|
||||||
>
|
|
||||||
{literal}
|
|
||||||
</Text>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = size;
|
|
||||||
const height = size;
|
|
||||||
|
|
||||||
// Android can't change the size of an image after its first render, so
|
|
||||||
// force a new image to be rendered when the size changes
|
|
||||||
const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
|
|
||||||
|
|
||||||
if (unicode && !imageUrl) {
|
|
||||||
const codeArray = unicode.split('-');
|
|
||||||
const code = codeArray.reduce((acc, c) => {
|
|
||||||
return acc + String.fromCodePoint(parseInt(c, 16));
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
style={[textStyle, {fontSize: size}]}
|
|
||||||
testID={testID}
|
|
||||||
>
|
|
||||||
{code}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageUrl) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FastImage
|
|
||||||
key={key}
|
|
||||||
style={[customEmojiStyle, {width, height}]}
|
|
||||||
source={{uri: imageUrl}}
|
|
||||||
onError={this.onError}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
|
||||||
testID={testID}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
116
app/components/emoji/emoji.tsx
Normal file
116
app/components/emoji/emoji.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Platform,
|
||||||
|
StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextStyle,
|
||||||
|
} from 'react-native';
|
||||||
|
import FastImage, {ImageStyle} from 'react-native-fast-image';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Emoji text name.
|
||||||
|
*/
|
||||||
|
emojiName: string;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Image URL for the emoji.
|
||||||
|
*/
|
||||||
|
imageUrl: string;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set if this is a custom emoji.
|
||||||
|
*/
|
||||||
|
isCustomEmoji: boolean;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to render only the text and no image.
|
||||||
|
*/
|
||||||
|
displayTextOnly?: boolean;
|
||||||
|
literal?: string;
|
||||||
|
size?: number;
|
||||||
|
textStyle?: StyleProp<TextStyle>;
|
||||||
|
unicode?: string;
|
||||||
|
customEmojiStyle?: StyleProp<ImageStyle>;
|
||||||
|
testID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Emoji: React.FC<Props> = (props: Props) => {
|
||||||
|
const {
|
||||||
|
customEmojiStyle,
|
||||||
|
displayTextOnly,
|
||||||
|
imageUrl,
|
||||||
|
literal,
|
||||||
|
unicode,
|
||||||
|
testID,
|
||||||
|
textStyle,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
let size = props.size;
|
||||||
|
let fontSize = size;
|
||||||
|
if (!size && textStyle) {
|
||||||
|
const flatten = StyleSheet.flatten(textStyle);
|
||||||
|
fontSize = flatten.fontSize;
|
||||||
|
size = fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayTextOnly) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={textStyle}
|
||||||
|
testID={testID}
|
||||||
|
>
|
||||||
|
{literal}
|
||||||
|
</Text>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = size;
|
||||||
|
const height = size;
|
||||||
|
|
||||||
|
if (unicode && !imageUrl) {
|
||||||
|
const codeArray = unicode.split('-');
|
||||||
|
const code = codeArray.reduce((acc, c) => {
|
||||||
|
return acc + String.fromCodePoint(parseInt(c, 16));
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={[textStyle, {fontSize: size}]}
|
||||||
|
testID={testID}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android can't change the size of an image after its first render, so
|
||||||
|
// force a new image to be rendered when the size changes
|
||||||
|
const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FastImage
|
||||||
|
key={key}
|
||||||
|
style={[customEmojiStyle, {width, height}]}
|
||||||
|
source={{uri: imageUrl}}
|
||||||
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
|
testID={testID}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Emoji.defaultProps = {
|
||||||
|
literal: '',
|
||||||
|
imageUrl: '',
|
||||||
|
isCustomEmoji: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Emoji;
|
||||||
@@ -8,12 +8,17 @@ import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
|||||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||||
import {Client4} from '@client/rest';
|
import {Client4} from '@client/rest';
|
||||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||||
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
|
|
||||||
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
|
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from '@utils/emojis';
|
||||||
|
|
||||||
import Emoji from './emoji';
|
import Emoji from './emoji';
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
type OwnProps = {
|
||||||
|
emojiName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
const emojiName = ownProps.emojiName;
|
const emojiName = ownProps.emojiName;
|
||||||
const customEmojis = getCustomEmojisByName(state);
|
const customEmojis = getCustomEmojisByName(state);
|
||||||
@@ -24,7 +29,7 @@ function mapStateToProps(state, ownProps) {
|
|||||||
let isCustomEmoji = false;
|
let isCustomEmoji = false;
|
||||||
let displayTextOnly = false;
|
let displayTextOnly = false;
|
||||||
if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) {
|
if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) {
|
||||||
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
|
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)!];
|
||||||
unicode = emoji.filename;
|
unicode = emoji.filename;
|
||||||
if (BuiltInEmojis.includes(emojiName)) {
|
if (BuiltInEmojis.includes(emojiName)) {
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
@@ -35,7 +40,7 @@ function mapStateToProps(state, ownProps) {
|
|||||||
}
|
}
|
||||||
} else if (customEmojis.has(emojiName) && serverUrl) {
|
} else if (customEmojis.has(emojiName) && serverUrl) {
|
||||||
const emoji = customEmojis.get(emojiName);
|
const emoji = customEmojis.get(emojiName);
|
||||||
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
|
imageUrl = Client4.getCustomEmojiImageUrl(emoji!.id);
|
||||||
isCustomEmoji = true;
|
isCustomEmoji = true;
|
||||||
} else {
|
} else {
|
||||||
displayTextOnly = state.entities.emojis.nonExistentEmoji.has(emojiName) ||
|
displayTextOnly = state.entities.emojis.nonExistentEmoji.has(emojiName) ||
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import shallowEqual from 'shallow-equals';
|
import shallowEqual from 'shallow-equals';
|
||||||
|
|
||||||
import Emoji from 'app/components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
|
|
||||||
export default class EmojiPickerRow extends Component {
|
export default class EmojiPickerRow extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import {
|
|||||||
|
|
||||||
import * as Utils from '@mm-redux/utils/file_utils';
|
import * as Utils from '@mm-redux/utils/file_utils';
|
||||||
|
|
||||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||||
import {isDocument, isImage} from 'app/utils/file';
|
import {isDocument, isImage} from '@utils/file';
|
||||||
import {calculateDimensions} from 'app/utils/images';
|
import {calculateDimensions} from '@utils/images';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
import FileAttachmentDocument from './file_attachment_document';
|
import FileAttachmentDocument from './file_attachment_document';
|
||||||
import FileAttachmentIcon from './file_attachment_icon';
|
import FileAttachmentIcon from './file_attachment_icon';
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
import AtMention from 'app/components/at_mention';
|
import AtMention from '@components/at_mention';
|
||||||
import ChannelLink from 'app/components/channel_link';
|
import ChannelLink from '@components/channel_link';
|
||||||
import Emoji from 'app/components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
import FormattedText from 'app/components/formatted_text';
|
import FormattedText from '@components/formatted_text';
|
||||||
import Hashtag from 'app/components/markdown/hashtag';
|
import Hashtag from '@components/markdown/hashtag';
|
||||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
|
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {getScheme} from 'app/utils/url';
|
import {getScheme} from '@utils/url';
|
||||||
|
|
||||||
import MarkdownBlockQuote from './markdown_block_quote';
|
import MarkdownBlockQuote from './markdown_block_quote';
|
||||||
import MarkdownCodeBlock from './markdown_code_block';
|
import MarkdownCodeBlock from './markdown_code_block';
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import React, {PureComponent} from 'react';
|
|||||||
import {Platform, Text, View} from 'react-native';
|
import {Platform, Text, View} from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Emoji from 'app/components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
import FormattedText from 'app/components/formatted_text';
|
import FormattedText from '@components/formatted_text';
|
||||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
|
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
export default class MarkdownEmoji extends PureComponent {
|
export default class MarkdownEmoji extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {calculateDimensions, isGifTooLarge, openGalleryAtIndex} from '@utils/ima
|
|||||||
import {generateId} from '@utils/file';
|
import {generateId} from '@utils/file';
|
||||||
|
|
||||||
import type {PostImage} from '@mm-redux/types/posts';
|
import type {PostImage} from '@mm-redux/types/posts';
|
||||||
|
import {FileInfo} from '@mm-redux/types/files';
|
||||||
|
|
||||||
type MarkdownTableImageProps = {
|
type MarkdownTableImageProps = {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
@@ -78,8 +79,11 @@ const MarkTableImage = ({disable, imagesMetadata, postId, serverURL, source}: Ma
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = [getFileInfo()];
|
const file = getFileInfo() as FileInfo;
|
||||||
openGalleryAtIndex(0, files);
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openGalleryAtIndex(0, [file]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onLoadFailed = useCallback(() => {
|
const onLoadFailed = useCallback(() => {
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
|
ellipsizeMode="tail"
|
||||||
key="footer_text"
|
key="footer_text"
|
||||||
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"color": "rgba(61,60,64,0.5)",
|
"color": "rgba(61,60,64,0.5)",
|
||||||
@@ -51,7 +53,9 @@ exports[`AttachmentFooter it matches snapshot when footer text is provided 1`] =
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
ellipsizeMode="tail"
|
||||||
key="footer_text"
|
key="footer_text"
|
||||||
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"color": "rgba(61,60,64,0.5)",
|
"color": "rgba(61,60,64,0.5)",
|
||||||
@@ -67,43 +71,3 @@ exports[`AttachmentFooter it matches snapshot when footer text is provided 1`] =
|
|||||||
exports[`AttachmentFooter it matches snapshot when no footer is provided 1`] = `""`;
|
exports[`AttachmentFooter it matches snapshot when no footer is provided 1`] = `""`;
|
||||||
|
|
||||||
exports[`AttachmentFooter it matches snapshot when only the footer icon is provided 1`] = `""`;
|
exports[`AttachmentFooter it matches snapshot when only the footer icon is provided 1`] = `""`;
|
||||||
|
|
||||||
exports[`AttachmentFooter it matches snapshot when the footer is longer than the maximum length 1`] = `
|
|
||||||
<View
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"flex": 1,
|
|
||||||
"flexDirection": "row",
|
|
||||||
"marginTop": 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FastImage
|
|
||||||
key="footer_icon"
|
|
||||||
source={
|
|
||||||
Object {
|
|
||||||
"uri": "https://images.com/image.png",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"height": 12,
|
|
||||||
"marginRight": 5,
|
|
||||||
"marginTop": 1,
|
|
||||||
"width": 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
key="footer_text"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"color": "rgba(61,60,64,0.5)",
|
|
||||||
"fontSize": 11,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
`;
|
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import ActionButton from './action_button';
|
import ActionButton from './action_button';
|
||||||
import {changeOpacity} from 'app/utils/theme';
|
|
||||||
import {getStatusColors} from 'app/utils/message_attachment_colors';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||||
|
|
||||||
import Preferences from '@mm-redux/constants/preferences';
|
import Preferences from '@mm-redux/constants/preferences';
|
||||||
|
|
||||||
@@ -106,7 +108,6 @@ describe('ActionButton', () => {
|
|||||||
cookie: '',
|
cookie: '',
|
||||||
name: buttonConfig.name,
|
name: buttonConfig.name,
|
||||||
postId: buttonConfig.id,
|
postId: buttonConfig.id,
|
||||||
buttonColor: buttonConfig.style,
|
|
||||||
theme: Preferences.THEMES.default,
|
theme: Preferences.THEMES.default,
|
||||||
actions: {
|
actions: {
|
||||||
doPostActionWithCookie: jest.fn(),
|
doPostActionWithCookie: jest.fn(),
|
||||||
@@ -2,31 +2,33 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
import React, {PureComponent} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Button from 'react-native-button';
|
import Button from 'react-native-button';
|
||||||
|
|
||||||
import {preventDoubleTap} from 'app/utils/tap';
|
|
||||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
|
||||||
import {getStatusColors} from 'app/utils/message_attachment_colors';
|
|
||||||
import ActionButtonText from './action_button_text';
|
import ActionButtonText from './action_button_text';
|
||||||
|
|
||||||
export default class ActionButton extends PureComponent {
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
static propTypes = {
|
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||||
actions: PropTypes.shape({
|
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||||
doPostActionWithCookie: PropTypes.func.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
postId: PropTypes.string.isRequired,
|
|
||||||
theme: PropTypes.object.isRequired,
|
|
||||||
cookie: PropTypes.string,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
buttonColor: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
import {ActionResult} from '@mm-redux/types/actions';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
actions: {
|
||||||
|
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<ActionResult>;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
postId: string;
|
||||||
|
theme: Theme,
|
||||||
|
cookie?: string,
|
||||||
|
disabled?: boolean,
|
||||||
|
buttonColor?: string,
|
||||||
|
}
|
||||||
|
export default class ActionButton extends PureComponent<Props> {
|
||||||
handleActionPress = preventDoubleTap(() => {
|
handleActionPress = preventDoubleTap(() => {
|
||||||
const {actions, id, postId, cookie} = this.props;
|
const {actions, id, postId, cookie} = this.props;
|
||||||
actions.doPostActionWithCookie(postId, id, cookie);
|
actions.doPostActionWithCookie(postId, id, cookie || '');
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -58,7 +60,7 @@ export default class ActionButton extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
const STATUS_COLORS = getStatusColors(theme);
|
const STATUS_COLORS = getStatusColors(theme);
|
||||||
return {
|
return {
|
||||||
button: {
|
button: {
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import ActionButtonText from './action_button_text';
|
import ActionButtonText from './action_button_text';
|
||||||
|
|
||||||
describe('ActionButtonText emojis', () => {
|
describe('ActionButtonText emojis', () => {
|
||||||
@@ -3,13 +3,14 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {Text, View, StyleSheet} from 'react-native';
|
import {Text, View, StyleSheet, StyleProp, TextStyle} from 'react-native';
|
||||||
import Emoji from 'app/components/emoji';
|
|
||||||
import {getEmoticonName} from 'app/utils/emoji_utils';
|
|
||||||
import {reEmoji, reEmoticon, reMain} from 'app/constants/emoji';
|
|
||||||
|
|
||||||
export default function ActionButtonText({message, style}) {
|
import Emoji from '@components/emoji';
|
||||||
const components = [];
|
import {reEmoji, reEmoticon, reMain} from '@constants/emoji';
|
||||||
|
import {getEmoticonName} from '@utils/emoji_utils';
|
||||||
|
|
||||||
|
export default function ActionButtonText({message, style}: {message: string; style: StyleProp<TextStyle>}) {
|
||||||
|
const components = [] as React.ReactNode[];
|
||||||
|
|
||||||
let text = message;
|
let text = message;
|
||||||
while (text) {
|
while (text) {
|
||||||
@@ -49,6 +50,9 @@ export default function ActionButtonText({message, style}) {
|
|||||||
// This is plain text, so capture as much text as possible until we hit the next possible emoji. Note that
|
// This is plain text, so capture as much text as possible until we hit the next possible emoji. Note that
|
||||||
// reMain always captures at least one character, so text will always be getting shorter
|
// reMain always captures at least one character, so text will always be getting shorter
|
||||||
match = text.match(reMain);
|
match = text.match(reMain);
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
components.push(
|
components.push(
|
||||||
<Text
|
<Text
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {bindActionCreators} from 'redux';
|
|
||||||
import {connect} from 'react-redux';
|
|
||||||
|
|
||||||
import {doPostActionWithCookie} from '@mm-redux/actions/posts';
|
|
||||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
|
||||||
|
|
||||||
import ActionButton from './action_button';
|
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
|
||||||
return {
|
|
||||||
theme: getTheme(state),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
actions: bindActionCreators({
|
|
||||||
doPostActionWithCookie,
|
|
||||||
}, dispatch),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
|
|
||||||
32
app/components/message_attachments/action_button/index.ts
Normal file
32
app/components/message_attachments/action_button/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
|
import ActionButton from './action_button';
|
||||||
|
|
||||||
|
import {doPostActionWithCookie} from '@mm-redux/actions/posts';
|
||||||
|
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||||
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
|
import {ActionFunc, ActionResult} from '@mm-redux/types/actions';
|
||||||
|
|
||||||
|
function mapStateToProps(state: GlobalState) {
|
||||||
|
return {
|
||||||
|
theme: getTheme(state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type Actions = {
|
||||||
|
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string | undefined) => Promise<ActionResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||||
|
doPostActionWithCookie,
|
||||||
|
}, dispatch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import AutocompleteSelector from 'app/components/autocomplete_selector';
|
|
||||||
|
|
||||||
export default class ActionMenu extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
actions: PropTypes.shape({
|
|
||||||
selectAttachmentMenuAction: PropTypes.func.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
dataSource: PropTypes.string,
|
|
||||||
defaultOption: PropTypes.string,
|
|
||||||
options: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
postId: PropTypes.string.isRequired,
|
|
||||||
selected: PropTypes.object,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
let selected;
|
|
||||||
if (props.defaultOption && props.options) {
|
|
||||||
selected = props.options.find((option) => option.value === props.defaultOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
selected,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (props.selected && props.selected !== state.selected) {
|
|
||||||
return {
|
|
||||||
selected: props.selected,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelect = (selected) => {
|
|
||||||
if (!selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
actions,
|
|
||||||
id,
|
|
||||||
postId,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
actions.selectAttachmentMenuAction(postId, id, selected.text, selected.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
dataSource,
|
|
||||||
options,
|
|
||||||
disabled,
|
|
||||||
} = this.props;
|
|
||||||
const {selected} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutocompleteSelector
|
|
||||||
placeholder={name}
|
|
||||||
dataSource={dataSource}
|
|
||||||
options={options}
|
|
||||||
selected={selected}
|
|
||||||
onSelected={this.handleSelect}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ describe('ActionMenu', () => {
|
|||||||
test('should start with nothing selected when no default is selected', () => {
|
test('should start with nothing selected when no default is selected', () => {
|
||||||
const wrapper = shallow(<ActionMenu {...baseProps}/>);
|
const wrapper = shallow(<ActionMenu {...baseProps}/>);
|
||||||
|
|
||||||
expect(wrapper.state('selected')).toBeUndefined();
|
expect(wrapper.props().selected).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should set selected based on default option', () => {
|
test('should set selected based on default option', () => {
|
||||||
@@ -39,7 +39,7 @@ describe('ActionMenu', () => {
|
|||||||
};
|
};
|
||||||
const wrapper = shallow(<ActionMenu {...props}/>);
|
const wrapper = shallow(<ActionMenu {...props}/>);
|
||||||
|
|
||||||
expect(wrapper.state('selected')).toBe(props.options[1]);
|
expect(wrapper.props().selected).toBe(props.options[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should start with previous value selected', () => {
|
test('should start with previous value selected', () => {
|
||||||
@@ -50,7 +50,7 @@ describe('ActionMenu', () => {
|
|||||||
};
|
};
|
||||||
const wrapper = shallow(<ActionMenu {...props}/>);
|
const wrapper = shallow(<ActionMenu {...props}/>);
|
||||||
|
|
||||||
expect(wrapper.state('selected')).toBe(props.selected);
|
expect(wrapper.props().selected).toBe(props.selected);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('disabled works', () => {
|
test('disabled works', () => {
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||||
|
import {PostActionOption} from '@mm-redux/types/integration_actions';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
actions: {
|
||||||
|
selectAttachmentMenuAction: (postId: string, actionId: string, text: string, value: string) => void;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
dataSource?: string;
|
||||||
|
defaultOption?: string;
|
||||||
|
options?: PostActionOption[];
|
||||||
|
postId: string;
|
||||||
|
selected?: PostActionOption;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionMenu: React.FC<Props> = (props: Props) => {
|
||||||
|
let selected: PostActionOption | undefined;
|
||||||
|
if (props.defaultOption && props.options) {
|
||||||
|
selected = props.options.find((option) => option.value === props.defaultOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selected) {
|
||||||
|
selected = props.selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (selectedItem?: PostActionOption) => {
|
||||||
|
if (!selectedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
actions,
|
||||||
|
id,
|
||||||
|
postId,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
actions.selectAttachmentMenuAction(postId, id, selectedItem.text, selectedItem.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
dataSource,
|
||||||
|
options,
|
||||||
|
disabled,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutocompleteSelector
|
||||||
|
placeholder={name}
|
||||||
|
dataSource={dataSource}
|
||||||
|
options={options}
|
||||||
|
selected={selected}
|
||||||
|
onSelected={handleSelect}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionMenu;
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators, Dispatch} from 'redux';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
import {selectAttachmentMenuAction} from 'app/actions/views/post';
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
|
|
||||||
|
import {selectAttachmentMenuAction} from '@actions/views/post';
|
||||||
|
|
||||||
import ActionMenu from './action_menu';
|
import ActionMenu from './action_menu';
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
type OwnProps = {
|
||||||
|
postId: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||||
const actions = state.views.post.submittedMenuActions[ownProps.postId];
|
const actions = state.views.post.submittedMenuActions[ownProps.postId];
|
||||||
const selected = actions?.[ownProps.id];
|
const selected = actions?.[ownProps.id];
|
||||||
|
|
||||||
@@ -17,7 +24,7 @@ function mapStateToProps(state, ownProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators({
|
actions: bindActionCreators({
|
||||||
selectAttachmentMenuAction,
|
selectAttachmentMenuAction,
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import ActionMenu from './action_menu';
|
|
||||||
import ActionButton from './action_button';
|
|
||||||
|
|
||||||
export default class AttachmentActions extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
actions: PropTypes.array,
|
|
||||||
postId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
actions,
|
|
||||||
postId,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!actions?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = [];
|
|
||||||
|
|
||||||
actions.forEach((action) => {
|
|
||||||
if (!action.id || !action.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case 'select':
|
|
||||||
content.push(
|
|
||||||
<ActionMenu
|
|
||||||
key={action.id}
|
|
||||||
id={action.id}
|
|
||||||
name={action.name}
|
|
||||||
dataSource={action.data_source}
|
|
||||||
defaultOption={action.default_option}
|
|
||||||
options={action.options}
|
|
||||||
postId={postId}
|
|
||||||
disabled={action.disabled}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'button':
|
|
||||||
default:
|
|
||||||
content.push(
|
|
||||||
<ActionButton
|
|
||||||
key={action.id}
|
|
||||||
id={action.id}
|
|
||||||
cookie={action.cookie}
|
|
||||||
name={action.name}
|
|
||||||
postId={postId}
|
|
||||||
disabled={action.disabled}
|
|
||||||
buttonColor={action.style}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return content.length ? content : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
app/components/message_attachments/attachment_actions.tsx
Normal file
65
app/components/message_attachments/attachment_actions.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ActionMenu from './action_menu';
|
||||||
|
import ActionButton from './action_button';
|
||||||
|
|
||||||
|
import {PostAction} from '@mm-redux/types/integration_actions';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
actions?: PostAction[];
|
||||||
|
postId: string;
|
||||||
|
}
|
||||||
|
export default function AttachmentActions(props: Props) {
|
||||||
|
const {
|
||||||
|
actions,
|
||||||
|
postId,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!actions?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = [] as React.ReactNode[];
|
||||||
|
|
||||||
|
actions.forEach((action) => {
|
||||||
|
if (!action.id || !action.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'select':
|
||||||
|
content.push(
|
||||||
|
<ActionMenu
|
||||||
|
key={action.id}
|
||||||
|
id={action.id}
|
||||||
|
name={action.name}
|
||||||
|
dataSource={action.data_source}
|
||||||
|
defaultOption={action.default_option}
|
||||||
|
options={action.options}
|
||||||
|
postId={postId}
|
||||||
|
disabled={action.disabled}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'button':
|
||||||
|
default:
|
||||||
|
content.push(
|
||||||
|
<ActionButton
|
||||||
|
key={action.id}
|
||||||
|
id={action.id}
|
||||||
|
cookie={action.cookie}
|
||||||
|
name={action.name}
|
||||||
|
postId={postId}
|
||||||
|
disabled={action.disabled}
|
||||||
|
buttonColor={action.style}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return content.length ? (<>{content}</>) : null;
|
||||||
|
}
|
||||||
@@ -5,19 +5,18 @@ import React, {PureComponent} from 'react';
|
|||||||
import {Alert, Text, View} from 'react-native';
|
import {Alert, Text, View} from 'react-native';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {tryOpenURL} from '@utils/url';
|
import {tryOpenURL} from '@utils/url';
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
export default class AttachmentAuthor extends PureComponent {
|
type Props = {
|
||||||
static propTypes = {
|
icon?: string;
|
||||||
icon: PropTypes.string,
|
link?: string;
|
||||||
link: PropTypes.string,
|
name?: string;
|
||||||
name: PropTypes.string,
|
theme: Theme;
|
||||||
theme: PropTypes.object.isRequired,
|
}
|
||||||
};
|
export default class AttachmentAuthor extends PureComponent<Props> {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
@@ -81,7 +80,7 @@ export default class AttachmentAuthor extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import {Text, View} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Markdown from '@components/markdown';
|
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
|
||||||
|
|
||||||
export default class AttachmentFields extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
||||||
blockStyles: PropTypes.object.isRequired,
|
|
||||||
fields: PropTypes.array,
|
|
||||||
metadata: PropTypes.object,
|
|
||||||
onPermalinkPress: PropTypes.func,
|
|
||||||
textStyles: PropTypes.object.isRequired,
|
|
||||||
theme: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
baseTextStyle,
|
|
||||||
blockStyles,
|
|
||||||
fields,
|
|
||||||
metadata,
|
|
||||||
onPermalinkPress,
|
|
||||||
textStyles,
|
|
||||||
theme,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!fields?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = getStyleSheet(theme);
|
|
||||||
const fieldTables = [];
|
|
||||||
|
|
||||||
let fieldInfos = [];
|
|
||||||
let rowPos = 0;
|
|
||||||
let lastWasLong = false;
|
|
||||||
let nrTables = 0;
|
|
||||||
|
|
||||||
fields.forEach((field, i) => {
|
|
||||||
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
|
|
||||||
fieldTables.push(
|
|
||||||
<View
|
|
||||||
key={`attachment__table__${nrTables}`}
|
|
||||||
style={style.field}
|
|
||||||
>
|
|
||||||
{fieldInfos}
|
|
||||||
</View>,
|
|
||||||
);
|
|
||||||
fieldInfos = [];
|
|
||||||
rowPos = 0;
|
|
||||||
nrTables += 1;
|
|
||||||
lastWasLong = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldInfos.push(
|
|
||||||
<View
|
|
||||||
style={style.flex}
|
|
||||||
key={`attachment__field-${i}__${nrTables}`}
|
|
||||||
>
|
|
||||||
{Boolean(field.title) && (
|
|
||||||
<View
|
|
||||||
style={style.headingContainer}
|
|
||||||
key={`attachment__field-caption-${i}__${nrTables}`}
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<Text style={style.heading}>
|
|
||||||
{field.title}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View
|
|
||||||
style={style.flex}
|
|
||||||
key={`attachment__field-${i}__${nrTables}`}
|
|
||||||
>
|
|
||||||
<Markdown
|
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
textStyles={textStyles}
|
|
||||||
blockStyles={blockStyles}
|
|
||||||
disableGallery={true}
|
|
||||||
imagesMetadata={metadata?.images}
|
|
||||||
value={(field.value || '')}
|
|
||||||
onPermalinkPress={onPermalinkPress}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>,
|
|
||||||
);
|
|
||||||
|
|
||||||
rowPos += 1;
|
|
||||||
lastWasLong = !(field.short === true);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fieldInfos.length > 0) { // Flush last fields
|
|
||||||
fieldTables.push(
|
|
||||||
<View
|
|
||||||
key={`attachment__table__${nrTables}`}
|
|
||||||
style={style.table}
|
|
||||||
>
|
|
||||||
{fieldInfos}
|
|
||||||
</View>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
{fieldTables}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|
||||||
return {
|
|
||||||
field: {
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
flex: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
headingContainer: {
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 5,
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
|
||||||
heading: {
|
|
||||||
color: theme.centerChannelColor,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
145
app/components/message_attachments/attachment_fields.tsx
Normal file
145
app/components/message_attachments/attachment_fields.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import Markdown from '@components/markdown';
|
||||||
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
import {MessageAttachmentField} from '@mm-redux/types/message_attachments';
|
||||||
|
import {PostMetadata} from '@mm-redux/types/posts';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
baseTextStyle: StyleProp<TextStyle>,
|
||||||
|
blockStyles?: StyleProp<ViewStyle>[],
|
||||||
|
fields?: MessageAttachmentField[],
|
||||||
|
metadata?: PostMetadata,
|
||||||
|
onPermalinkPress?: () => void,
|
||||||
|
textStyles?: StyleProp<TextStyle>[],
|
||||||
|
theme: Theme,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AttachmentFields(props: Props) {
|
||||||
|
const {
|
||||||
|
baseTextStyle,
|
||||||
|
blockStyles,
|
||||||
|
fields,
|
||||||
|
metadata,
|
||||||
|
onPermalinkPress,
|
||||||
|
textStyles,
|
||||||
|
theme,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!fields?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
const fieldTables = [];
|
||||||
|
|
||||||
|
let fieldInfos = [] as React.ReactNode[];
|
||||||
|
let rowPos = 0;
|
||||||
|
let lastWasLong = false;
|
||||||
|
let nrTables = 0;
|
||||||
|
|
||||||
|
fields.forEach((field, i) => {
|
||||||
|
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
|
||||||
|
fieldTables.push(
|
||||||
|
<View
|
||||||
|
key={`attachment__table__${nrTables}`}
|
||||||
|
style={style.field}
|
||||||
|
>
|
||||||
|
{fieldInfos}
|
||||||
|
</View>,
|
||||||
|
);
|
||||||
|
fieldInfos = [];
|
||||||
|
rowPos = 0;
|
||||||
|
nrTables += 1;
|
||||||
|
lastWasLong = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldInfos.push(
|
||||||
|
<View
|
||||||
|
style={style.flex}
|
||||||
|
key={`attachment__field-${i}__${nrTables}`}
|
||||||
|
>
|
||||||
|
{Boolean(field.title) && (
|
||||||
|
<View
|
||||||
|
style={style.headingContainer}
|
||||||
|
key={`attachment__field-caption-${i}__${nrTables}`}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Text style={style.heading}>
|
||||||
|
{field.title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View
|
||||||
|
style={style.flex}
|
||||||
|
key={`attachment__field-${i}__${nrTables}`}
|
||||||
|
>
|
||||||
|
<Markdown
|
||||||
|
|
||||||
|
//TODO: remove conversion when markdown is migrated to typescript
|
||||||
|
baseTextStyle={baseTextStyle as any}
|
||||||
|
textStyles={textStyles}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
disableGallery={true}
|
||||||
|
imagesMetadata={metadata?.images}
|
||||||
|
value={(field.value || '')}
|
||||||
|
onPermalinkPress={onPermalinkPress}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>,
|
||||||
|
);
|
||||||
|
|
||||||
|
rowPos += 1;
|
||||||
|
lastWasLong = !(field.short === true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fieldInfos.length > 0) { // Flush last fields
|
||||||
|
fieldTables.push(
|
||||||
|
<View
|
||||||
|
key={`attachment__table__${nrTables}`}
|
||||||
|
style={style.table}
|
||||||
|
>
|
||||||
|
{fieldInfos}
|
||||||
|
</View>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{fieldTables}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
return {
|
||||||
|
field: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
flex: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
headingContainer: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginBottom: 5,
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import {Text, View, Platform} from 'react-native';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import truncate from 'lodash/truncate';
|
|
||||||
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
|
||||||
import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment';
|
|
||||||
|
|
||||||
export default class AttachmentFooter extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
text: PropTypes.string,
|
|
||||||
icon: PropTypes.string,
|
|
||||||
theme: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
text,
|
|
||||||
icon,
|
|
||||||
theme,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = getStyleSheet(theme);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={style.container}>
|
|
||||||
{Boolean(icon) &&
|
|
||||||
<FastImage
|
|
||||||
source={{uri: icon}}
|
|
||||||
key='footer_icon'
|
|
||||||
style={style.icon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<Text
|
|
||||||
key='footer_text'
|
|
||||||
style={style.text}
|
|
||||||
>
|
|
||||||
{truncate(text, {length: MAX_ATTACHMENT_FOOTER_LENGTH, omission: '…'})}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|
||||||
return {
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginTop: 5,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
height: 12,
|
|
||||||
width: 12,
|
|
||||||
marginRight: 5,
|
|
||||||
...Platform.select({
|
|
||||||
ios: {
|
|
||||||
marginTop: 1,
|
|
||||||
},
|
|
||||||
android: {
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
|
||||||
fontSize: 11,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment';
|
|
||||||
import AttachmentFooter from './attachment_footer';
|
import AttachmentFooter from './attachment_footer';
|
||||||
|
|
||||||
import Preferences from '@mm-redux/constants/preferences';
|
import Preferences from '@mm-redux/constants/preferences';
|
||||||
@@ -51,14 +50,4 @@ describe('AttachmentFooter', () => {
|
|||||||
const wrapper = shallow(<AttachmentFooter {...baseProps}/>);
|
const wrapper = shallow(<AttachmentFooter {...baseProps}/>);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it matches snapshot when the footer is longer than the maximum length', () => {
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
text: 'a'.repeat(MAX_ATTACHMENT_FOOTER_LENGTH + 1),
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<AttachmentFooter {...props}/>);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
77
app/components/message_attachments/attachment_footer.tsx
Normal file
77
app/components/message_attachments/attachment_footer.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {Text, View, Platform} from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
text?: string;
|
||||||
|
icon?: string;
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AttachmentFooter(props: Props) {
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
theme,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style.container}>
|
||||||
|
{Boolean(icon) &&
|
||||||
|
<FastImage
|
||||||
|
source={{uri: icon}}
|
||||||
|
key='footer_icon'
|
||||||
|
style={style.icon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<Text
|
||||||
|
key='footer_text'
|
||||||
|
style={style.text}
|
||||||
|
ellipsizeMode='tail'
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
height: 12,
|
||||||
|
width: 12,
|
||||||
|
marginRight: 5,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
marginTop: 1,
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import Preferences from '@mm-redux/constants/preferences';
|
import Preferences from '@mm-redux/constants/preferences';
|
||||||
|
|
||||||
import AttachmentImage from './index';
|
import AttachmentImage, {State, Props} from './index';
|
||||||
|
|
||||||
describe('AttachmentImage', () => {
|
describe('AttachmentImage', () => {
|
||||||
const baseProps = {
|
const baseProps = {
|
||||||
@@ -15,7 +15,7 @@ describe('AttachmentImage', () => {
|
|||||||
imageMetadata: {width: 32, height: 32},
|
imageMetadata: {width: 32, height: 32},
|
||||||
imageUrl: 'https://images.com/image.png',
|
imageUrl: 'https://images.com/image.png',
|
||||||
theme: Preferences.THEMES.default,
|
theme: Preferences.THEMES.default,
|
||||||
};
|
} as Props;
|
||||||
|
|
||||||
test('it matches snapshot', () => {
|
test('it matches snapshot', () => {
|
||||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
||||||
@@ -23,7 +23,7 @@ describe('AttachmentImage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it sets state based on props', () => {
|
test('it sets state based on props', () => {
|
||||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...baseProps}/>);
|
||||||
|
|
||||||
const state = wrapper.state();
|
const state = wrapper.state();
|
||||||
expect(state.hasImage).toBe(true);
|
expect(state.hasImage).toBe(true);
|
||||||
@@ -32,8 +32,11 @@ describe('AttachmentImage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it does not render image if no imageUrl is provided', () => {
|
test('it does not render image if no imageUrl is provided', () => {
|
||||||
const props = {...baseProps, imageUrl: null, imageMetadata: null};
|
const props = {...baseProps};
|
||||||
const wrapper = shallow(<AttachmentImage {...props}/>);
|
delete props.imageUrl;
|
||||||
|
delete props.imageMetadata;
|
||||||
|
|
||||||
|
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...props}/>);
|
||||||
|
|
||||||
const state = wrapper.state();
|
const state = wrapper.state();
|
||||||
expect(state.hasImage).toBe(false);
|
expect(state.hasImage).toBe(false);
|
||||||
@@ -41,7 +44,7 @@ describe('AttachmentImage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it updates image when imageUrl prop changes', () => {
|
test('it updates image when imageUrl prop changes', () => {
|
||||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...baseProps}/>);
|
||||||
|
|
||||||
wrapper.setProps({
|
wrapper.setProps({
|
||||||
imageUrl: 'https://someothersite.com/picture.png',
|
imageUrl: 'https://someothersite.com/picture.png',
|
||||||
@@ -58,7 +61,7 @@ describe('AttachmentImage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it does not update image when an unrelated prop changes', () => {
|
test('it does not update image when an unrelated prop changes', () => {
|
||||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...baseProps}/>);
|
||||||
|
|
||||||
wrapper.setProps({
|
wrapper.setProps({
|
||||||
theme: {...Preferences.THEMES.default},
|
theme: {...Preferences.THEMES.default},
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
import React, {PureComponent} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
|
|
||||||
import ProgressiveImage from '@components/progressive_image';
|
import ProgressiveImage from '@components/progressive_image';
|
||||||
@@ -11,20 +10,37 @@ import {generateId} from '@utils/file';
|
|||||||
import {isGifTooLarge, openGalleryAtIndex, calculateDimensions} from '@utils/images';
|
import {isGifTooLarge, openGalleryAtIndex, calculateDimensions} from '@utils/images';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
import {PostImage} from '@mm-redux/types/posts';
|
||||||
|
import {FileInfo} from '@mm-redux/types/files';
|
||||||
|
|
||||||
const VIEWPORT_IMAGE_OFFSET = 100;
|
const VIEWPORT_IMAGE_OFFSET = 100;
|
||||||
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
|
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
|
||||||
|
|
||||||
export default class AttachmentImage extends PureComponent {
|
export type Props = {
|
||||||
static propTypes = {
|
deviceHeight: number;
|
||||||
deviceHeight: PropTypes.number.isRequired,
|
deviceWidth: number;
|
||||||
deviceWidth: PropTypes.number.isRequired,
|
imageMetadata?: PostImage;
|
||||||
imageMetadata: PropTypes.object,
|
imageUrl?: string;
|
||||||
imageUrl: PropTypes.string,
|
postId?: string;
|
||||||
postId: PropTypes.string,
|
theme: Theme;
|
||||||
theme: PropTypes.object.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
export type State = {
|
||||||
|
hasImage: boolean;
|
||||||
|
imageUri: string | null;
|
||||||
|
originalHeight?: number;
|
||||||
|
originalWidth?: number;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AttachmentImage extends PureComponent<Props, State> {
|
||||||
|
private fileId: string;
|
||||||
|
private mounted = false;
|
||||||
|
private maxImageWidth = 0;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.fileId = generateId();
|
this.fileId = generateId();
|
||||||
@@ -48,7 +64,7 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
|
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
|
||||||
this.setImageUrl(this.props.imageUrl);
|
this.setImageUrl(this.props.imageUrl);
|
||||||
}
|
}
|
||||||
@@ -62,6 +78,16 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
originalWidth,
|
originalWidth,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
return {
|
||||||
|
id: this.fileId,
|
||||||
|
post_id: postId,
|
||||||
|
uri,
|
||||||
|
width: originalWidth,
|
||||||
|
height: originalHeight,
|
||||||
|
} as FileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
|
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
|
||||||
const extension = filename.split('.').pop();
|
const extension = filename.split('.').pop();
|
||||||
|
|
||||||
@@ -70,7 +96,7 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
filename = `${filename}${ext}`;
|
filename = `${filename}${ext}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const out = {
|
||||||
id: this.fileId,
|
id: this.fileId,
|
||||||
name: filename,
|
name: filename,
|
||||||
extension,
|
extension,
|
||||||
@@ -79,7 +105,8 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
uri,
|
uri,
|
||||||
width: originalWidth,
|
width: originalWidth,
|
||||||
height: originalHeight,
|
height: originalHeight,
|
||||||
};
|
} as FileInfo;
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreviewImage = () => {
|
handlePreviewImage = () => {
|
||||||
@@ -87,7 +114,7 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
openGalleryAtIndex(0, files);
|
openGalleryAtIndex(0, files);
|
||||||
};
|
};
|
||||||
|
|
||||||
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
|
setImageDimensions = (imageUri: string | null, dimensions: {width?: number; height?: number;}, originalWidth: number, originalHeight: number) => {
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
this.setState({
|
this.setState({
|
||||||
...dimensions,
|
...dimensions,
|
||||||
@@ -98,12 +125,12 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
|
setImageDimensionsFromMeta = (imageUri: string | null, imageMetadata: PostImage) => {
|
||||||
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
|
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
|
||||||
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
|
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
|
||||||
};
|
};
|
||||||
|
|
||||||
setImageUrl = (imageURL) => {
|
setImageUrl = (imageURL: string) => {
|
||||||
const {imageMetadata} = this.props;
|
const {imageMetadata} = this.props;
|
||||||
|
|
||||||
if (imageMetadata) {
|
if (imageMetadata) {
|
||||||
@@ -158,7 +185,7 @@ export default class AttachmentImage extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
container: {
|
container: {
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import {StyleSheet, View} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Markdown from '@components/markdown';
|
|
||||||
|
|
||||||
export default class AttachmentPreText extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
||||||
blockStyles: PropTypes.object.isRequired,
|
|
||||||
metadata: PropTypes.object,
|
|
||||||
onPermalinkPress: PropTypes.func,
|
|
||||||
textStyles: PropTypes.object.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
baseTextStyle,
|
|
||||||
blockStyles,
|
|
||||||
metadata,
|
|
||||||
onPermalinkPress,
|
|
||||||
value,
|
|
||||||
textStyles,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={style.container}>
|
|
||||||
<Markdown
|
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
textStyles={textStyles}
|
|
||||||
blockStyles={blockStyles}
|
|
||||||
disableGallery={true}
|
|
||||||
imagesMetadata={metadata?.images}
|
|
||||||
value={value}
|
|
||||||
onPermalinkPress={onPermalinkPress}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
marginTop: 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
54
app/components/message_attachments/attachment_pretext.tsx
Normal file
54
app/components/message_attachments/attachment_pretext.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import Markdown from '@components/markdown';
|
||||||
|
|
||||||
|
import {PostMetadata} from '@mm-redux/types/posts';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
baseTextStyle: StyleProp<TextStyle>;
|
||||||
|
blockStyles?: StyleProp<ViewStyle>[];
|
||||||
|
metadata?: PostMetadata;
|
||||||
|
onPermalinkPress?: () => void;
|
||||||
|
textStyles?: StyleProp<TextStyle>[];
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
export default function AttachmentPreText(props: Props) {
|
||||||
|
const {
|
||||||
|
baseTextStyle,
|
||||||
|
blockStyles,
|
||||||
|
metadata,
|
||||||
|
onPermalinkPress,
|
||||||
|
value,
|
||||||
|
textStyles,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style.container}>
|
||||||
|
<Markdown
|
||||||
|
|
||||||
|
// TODO: remove any when migrating Markdown to typescript
|
||||||
|
baseTextStyle={baseTextStyle as any}
|
||||||
|
textStyles={textStyles}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
disableGallery={true}
|
||||||
|
imagesMetadata={metadata?.images}
|
||||||
|
value={value}
|
||||||
|
onPermalinkPress={onPermalinkPress}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,30 +2,42 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
import React, {PureComponent} from 'react';
|
||||||
import {ScrollView, StyleSheet, View} from 'react-native';
|
import {LayoutChangeEvent, ScrollView, StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Markdown from '@components/markdown';
|
import Markdown from '@components/markdown';
|
||||||
import ShowMoreButton from '@components/show_more_button';
|
import ShowMoreButton from '@components/show_more_button';
|
||||||
|
|
||||||
|
import {PostMetadata} from '@mm-redux/types/posts';
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
const SHOW_MORE_HEIGHT = 60;
|
const SHOW_MORE_HEIGHT = 60;
|
||||||
|
|
||||||
export default class AttachmentText extends PureComponent {
|
type Props = {
|
||||||
static propTypes = {
|
baseTextStyle: StyleProp<TextStyle>,
|
||||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
blockStyles?: StyleProp<ViewStyle>[],
|
||||||
blockStyles: PropTypes.object.isRequired,
|
deviceHeight: number,
|
||||||
deviceHeight: PropTypes.number.isRequired,
|
hasThumbnail?: boolean,
|
||||||
hasThumbnail: PropTypes.bool,
|
metadata?: PostMetadata,
|
||||||
metadata: PropTypes.object,
|
onPermalinkPress?: () => void,
|
||||||
onPermalinkPress: PropTypes.func,
|
textStyles?: StyleProp<TextStyle>[],
|
||||||
textStyles: PropTypes.object.isRequired,
|
theme?: Theme,
|
||||||
theme: PropTypes.object,
|
value?: string,
|
||||||
value: PropTypes.string,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
type State = {
|
||||||
|
collapsed: boolean;
|
||||||
|
isLongText: boolean;
|
||||||
|
maxHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxHeight(deviceHeight: number) {
|
||||||
|
return Math.round((deviceHeight * 0.4) + SHOW_MORE_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AttachmentText extends PureComponent<Props, State> {
|
||||||
|
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||||
const {deviceHeight} = nextProps;
|
const {deviceHeight} = nextProps;
|
||||||
const maxHeight = Math.round((deviceHeight * 0.4) + SHOW_MORE_HEIGHT);
|
const maxHeight = getMaxHeight(deviceHeight);
|
||||||
|
|
||||||
if (maxHeight !== prevState.maxHeight) {
|
if (maxHeight !== prevState.maxHeight) {
|
||||||
return {
|
return {
|
||||||
@@ -36,16 +48,18 @@ export default class AttachmentText extends PureComponent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const maxHeight = getMaxHeight(props.deviceHeight);
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
isLongText: false,
|
isLongText: false,
|
||||||
|
maxHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLayout = (event) => {
|
handleLayout = (event: LayoutChangeEvent) => {
|
||||||
const {height} = event.nativeEvent.layout;
|
const {height} = event.nativeEvent.layout;
|
||||||
const {maxHeight} = this.state;
|
const {maxHeight} = this.state;
|
||||||
|
|
||||||
@@ -81,7 +95,7 @@ export default class AttachmentText extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<View style={hasThumbnail && style.container}>
|
<View style={hasThumbnail && style.container}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{maxHeight: (collapsed ? maxHeight : null), overflow: 'hidden'}}
|
style={{maxHeight: (collapsed ? maxHeight : undefined), overflow: 'hidden'}}
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
@@ -91,7 +105,9 @@ export default class AttachmentText extends PureComponent {
|
|||||||
removeClippedSubviews={isLongText && collapsed}
|
removeClippedSubviews={isLongText && collapsed}
|
||||||
>
|
>
|
||||||
<Markdown
|
<Markdown
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
|
// TODO: remove any when migrating Markdown to typescript
|
||||||
|
baseTextStyle={baseTextStyle as any}
|
||||||
textStyles={textStyles}
|
textStyles={textStyles}
|
||||||
blockStyles={blockStyles}
|
blockStyles={blockStyles}
|
||||||
disableGallery={true}
|
disableGallery={true}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import {StyleSheet, View} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
|
|
||||||
export default class AttachmentThumbnail extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
url: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {url: uri} = this.props;
|
|
||||||
|
|
||||||
if (!uri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={style.container}>
|
|
||||||
<FastImage
|
|
||||||
source={{uri}}
|
|
||||||
resizeMode='contain'
|
|
||||||
resizeMethod='scale'
|
|
||||||
style={style.image}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
position: 'absolute',
|
|
||||||
right: 10,
|
|
||||||
top: 10,
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
height: 45,
|
|
||||||
width: 45,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
41
app/components/message_attachments/attachment_thumbnail.tsx
Normal file
41
app/components/message_attachments/attachment_thumbnail.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
|
import {isValidUrl} from '@utils/url';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
export default function AttachmentThumbnail(props: Props) {
|
||||||
|
const {url: uri} = props;
|
||||||
|
|
||||||
|
if (!isValidUrl(uri)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style.container}>
|
||||||
|
<FastImage
|
||||||
|
source={{uri}}
|
||||||
|
resizeMode='contain'
|
||||||
|
style={style.image}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 10,
|
||||||
|
top: 10,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
height: 45,
|
||||||
|
width: 45,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,19 +4,19 @@
|
|||||||
import React, {PureComponent} from 'react';
|
import React, {PureComponent} from 'react';
|
||||||
import {Alert, Text, View} from 'react-native';
|
import {Alert, Text, View} from 'react-native';
|
||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Markdown from '@components/markdown';
|
import Markdown from '@components/markdown';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {tryOpenURL} from '@utils/url';
|
import {tryOpenURL} from '@utils/url';
|
||||||
|
|
||||||
export default class AttachmentTitle extends PureComponent {
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
static propTypes = {
|
|
||||||
link: PropTypes.string,
|
|
||||||
theme: PropTypes.object.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link?: string;
|
||||||
|
theme: Theme;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
export default class AttachmentTitle extends PureComponent<Props> {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
@@ -93,7 +93,7 @@ export default class AttachmentTitle extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
container: {
|
container: {
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import {View} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import MessageAttachment from './message_attachment';
|
|
||||||
|
|
||||||
export default class MessageAttachments extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
attachments: PropTypes.array.isRequired,
|
|
||||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
||||||
blockStyles: PropTypes.object,
|
|
||||||
deviceHeight: PropTypes.number.isRequired,
|
|
||||||
deviceWidth: PropTypes.number.isRequired,
|
|
||||||
postId: PropTypes.string.isRequired,
|
|
||||||
metadata: PropTypes.object,
|
|
||||||
onHashtagPress: PropTypes.func,
|
|
||||||
onPermalinkPress: PropTypes.func,
|
|
||||||
theme: PropTypes.object,
|
|
||||||
textStyles: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
attachments,
|
|
||||||
baseTextStyle,
|
|
||||||
blockStyles,
|
|
||||||
deviceHeight,
|
|
||||||
deviceWidth,
|
|
||||||
metadata,
|
|
||||||
onHashtagPress,
|
|
||||||
onPermalinkPress,
|
|
||||||
postId,
|
|
||||||
theme,
|
|
||||||
textStyles,
|
|
||||||
} = this.props;
|
|
||||||
const content = [];
|
|
||||||
|
|
||||||
attachments.forEach((attachment, i) => {
|
|
||||||
content.push(
|
|
||||||
<MessageAttachment
|
|
||||||
attachment={attachment}
|
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
blockStyles={blockStyles}
|
|
||||||
deviceHeight={deviceHeight}
|
|
||||||
deviceWidth={deviceWidth}
|
|
||||||
key={'att_' + i}
|
|
||||||
metadata={metadata}
|
|
||||||
onHashtagPress={onHashtagPress}
|
|
||||||
onPermalinkPress={onPermalinkPress}
|
|
||||||
postId={postId}
|
|
||||||
theme={theme}
|
|
||||||
textStyles={textStyles}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{flex: 1, flexDirection: 'column'}}>
|
|
||||||
{content}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
app/components/message_attachments/index.tsx
Normal file
64
app/components/message_attachments/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import {MessageAttachment as MessageAttachmentType} from '@mm-redux/types/message_attachments';
|
||||||
|
import {PostMetadata} from '@mm-redux/types/posts';
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
|
import MessageAttachment from './message_attachment';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attachments: MessageAttachmentType[],
|
||||||
|
baseTextStyle?: StyleProp<TextStyle>,
|
||||||
|
blockStyles?: StyleProp<ViewStyle>[],
|
||||||
|
deviceHeight: number,
|
||||||
|
deviceWidth: number,
|
||||||
|
postId: string,
|
||||||
|
metadata?: PostMetadata,
|
||||||
|
onPermalinkPress?: () => void,
|
||||||
|
theme: Theme,
|
||||||
|
textStyles?: StyleProp<TextStyle>[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MessageAttachments(props: Props) {
|
||||||
|
const {
|
||||||
|
attachments,
|
||||||
|
baseTextStyle,
|
||||||
|
blockStyles,
|
||||||
|
deviceHeight,
|
||||||
|
deviceWidth,
|
||||||
|
metadata,
|
||||||
|
onPermalinkPress,
|
||||||
|
postId,
|
||||||
|
theme,
|
||||||
|
textStyles,
|
||||||
|
} = props;
|
||||||
|
const content = [] as React.ReactNode[];
|
||||||
|
|
||||||
|
attachments.forEach((attachment, i) => {
|
||||||
|
content.push(
|
||||||
|
<MessageAttachment
|
||||||
|
attachment={attachment}
|
||||||
|
baseTextStyle={baseTextStyle}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
deviceHeight={deviceHeight}
|
||||||
|
deviceWidth={deviceWidth}
|
||||||
|
key={'att_' + i}
|
||||||
|
metadata={metadata}
|
||||||
|
onPermalinkPress={onPermalinkPress}
|
||||||
|
postId={postId}
|
||||||
|
theme={theme}
|
||||||
|
textStyles={textStyles}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{flex: 1, flexDirection: 'column'}}>
|
||||||
|
{content}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {View} from 'react-native';
|
|
||||||
|
|
||||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
||||||
|
|
||||||
import AttachmentActions from './attachment_actions';
|
|
||||||
import AttachmentAuthor from './attachment_author';
|
|
||||||
import AttachmentFields from './attachment_fields';
|
|
||||||
import AttachmentImage from './attachment_image';
|
|
||||||
import AttachmentPreText from './attachment_pretext';
|
|
||||||
import AttachmentText from './attachment_text';
|
|
||||||
import AttachmentThumbnail from './attachment_thumbnail';
|
|
||||||
import AttachmentTitle from './attachment_title';
|
|
||||||
import AttachmentFooter from './attachment_footer';
|
|
||||||
|
|
||||||
export default class MessageAttachment extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
attachment: PropTypes.object.isRequired,
|
|
||||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
|
||||||
blockStyles: PropTypes.object,
|
|
||||||
deviceHeight: PropTypes.number.isRequired,
|
|
||||||
deviceWidth: PropTypes.number.isRequired,
|
|
||||||
metadata: PropTypes.object,
|
|
||||||
postId: PropTypes.string.isRequired,
|
|
||||||
onPermalinkPress: PropTypes.func,
|
|
||||||
theme: PropTypes.object,
|
|
||||||
textStyles: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
attachment,
|
|
||||||
baseTextStyle,
|
|
||||||
blockStyles,
|
|
||||||
deviceHeight,
|
|
||||||
deviceWidth,
|
|
||||||
metadata,
|
|
||||||
onPermalinkPress,
|
|
||||||
postId,
|
|
||||||
textStyles,
|
|
||||||
theme,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const style = getStyleSheet(theme);
|
|
||||||
const STATUS_COLORS = getStatusColors(theme);
|
|
||||||
const hasImage = Boolean(metadata?.images?.[attachment.image_url]);
|
|
||||||
|
|
||||||
let borderStyle;
|
|
||||||
if (attachment.color) {
|
|
||||||
if (attachment.color[0] === '#') {
|
|
||||||
borderStyle = {borderLeftColor: attachment.color};
|
|
||||||
} else if (STATUS_COLORS.hasOwnProperty(attachment.color)) {
|
|
||||||
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<AttachmentPreText
|
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
blockStyles={blockStyles}
|
|
||||||
metadata={metadata}
|
|
||||||
onPermalinkPress={onPermalinkPress}
|
|
||||||
textStyles={textStyles}
|
|
||||||
value={attachment.pretext}
|
|
||||||
/>
|
|
||||||
<View style={[style.container, style.border, borderStyle]}>
|
|
||||||
<AttachmentAuthor
|
|
||||||
icon={attachment.author_icon}
|
|
||||||
link={attachment.author_link}
|
|
||||||
name={attachment.author_name}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<AttachmentTitle
|
|
||||||
link={attachment.title_link}
|
|
||||||
theme={theme}
|
|
||||||
value={attachment.title}
|
|
||||||
/>
|
|
||||||
<AttachmentThumbnail url={attachment.thumb_url}/>
|
|
||||||
<AttachmentText
|
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
blockStyles={blockStyles}
|
|
||||||
deviceHeight={deviceHeight}
|
|
||||||
hasThumbnail={Boolean(attachment.thumb_url)}
|
|
||||||
metadata={metadata}
|
|
||||||
onPermalinkPress={onPermalinkPress}
|
|
||||||
textStyles={textStyles}
|
|
||||||
value={attachment.text}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<AttachmentFields
|
|
||||||
baseTextStyle={baseTextStyle}
|
|
||||||
blockStyles={blockStyles}
|
|
||||||
fields={attachment.fields}
|
|
||||||
metadata={metadata}
|
|
||||||
onPermalinkPress={onPermalinkPress}
|
|
||||||
textStyles={textStyles}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<AttachmentFooter
|
|
||||||
icon={attachment.footer_icon}
|
|
||||||
text={attachment.footer}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<AttachmentActions
|
|
||||||
actions={attachment.actions}
|
|
||||||
postId={postId}
|
|
||||||
/>
|
|
||||||
{hasImage &&
|
|
||||||
<AttachmentImage
|
|
||||||
deviceHeight={deviceHeight}
|
|
||||||
deviceWidth={deviceWidth}
|
|
||||||
imageUrl={attachment.image_url}
|
|
||||||
imageMetadata={metadata?.images?.[attachment.image_url]}
|
|
||||||
postId={postId}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|
||||||
return {
|
|
||||||
container: {
|
|
||||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.15),
|
|
||||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
|
||||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.15),
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
marginTop: 5,
|
|
||||||
padding: 12,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
borderLeftColor: changeOpacity(theme.linkColor, 0.6),
|
|
||||||
borderLeftWidth: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
147
app/components/message_attachments/message_attachment.tsx
Normal file
147
app/components/message_attachments/message_attachment.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import {MessageAttachment as MessageAttachmentType} from '@mm-redux/types/message_attachments';
|
||||||
|
import {PostMetadata} from '@mm-redux/types/posts';
|
||||||
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
|
import AttachmentActions from './attachment_actions';
|
||||||
|
import AttachmentAuthor from './attachment_author';
|
||||||
|
import AttachmentFields from './attachment_fields';
|
||||||
|
import AttachmentImage from './attachment_image';
|
||||||
|
import AttachmentPreText from './attachment_pretext';
|
||||||
|
import AttachmentText from './attachment_text';
|
||||||
|
import AttachmentThumbnail from './attachment_thumbnail';
|
||||||
|
import AttachmentTitle from './attachment_title';
|
||||||
|
import AttachmentFooter from './attachment_footer';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attachment: MessageAttachmentType,
|
||||||
|
baseTextStyle?: StyleProp<TextStyle>,
|
||||||
|
blockStyles?: StyleProp<ViewStyle>[],
|
||||||
|
deviceHeight: number,
|
||||||
|
deviceWidth: number,
|
||||||
|
postId: string,
|
||||||
|
metadata?: PostMetadata,
|
||||||
|
onPermalinkPress?: () => void,
|
||||||
|
theme: Theme,
|
||||||
|
textStyles?: StyleProp<TextStyle>[],
|
||||||
|
}
|
||||||
|
export default function MessageAttachment(props: Props) {
|
||||||
|
const {
|
||||||
|
attachment,
|
||||||
|
baseTextStyle,
|
||||||
|
blockStyles,
|
||||||
|
deviceHeight,
|
||||||
|
deviceWidth,
|
||||||
|
metadata,
|
||||||
|
onPermalinkPress,
|
||||||
|
postId,
|
||||||
|
textStyles,
|
||||||
|
theme,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
const STATUS_COLORS = getStatusColors(theme);
|
||||||
|
const hasImage = Boolean(metadata?.images?.[attachment.image_url]);
|
||||||
|
|
||||||
|
let borderStyle;
|
||||||
|
if (attachment.color) {
|
||||||
|
if (attachment.color[0] === '#') {
|
||||||
|
borderStyle = {borderLeftColor: attachment.color};
|
||||||
|
} else if (STATUS_COLORS.hasOwnProperty(attachment.color)) {
|
||||||
|
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<AttachmentPreText
|
||||||
|
baseTextStyle={baseTextStyle}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
metadata={metadata}
|
||||||
|
onPermalinkPress={onPermalinkPress}
|
||||||
|
textStyles={textStyles}
|
||||||
|
value={attachment.pretext}
|
||||||
|
/>
|
||||||
|
<View style={[style.container, style.border, borderStyle]}>
|
||||||
|
<AttachmentAuthor
|
||||||
|
icon={attachment.author_icon}
|
||||||
|
link={attachment.author_link}
|
||||||
|
name={attachment.author_name}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<AttachmentTitle
|
||||||
|
link={attachment.title_link}
|
||||||
|
theme={theme}
|
||||||
|
value={attachment.title}
|
||||||
|
/>
|
||||||
|
<AttachmentThumbnail url={attachment.thumb_url}/>
|
||||||
|
<AttachmentText
|
||||||
|
baseTextStyle={baseTextStyle}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
deviceHeight={deviceHeight}
|
||||||
|
hasThumbnail={Boolean(attachment.thumb_url)}
|
||||||
|
metadata={metadata}
|
||||||
|
onPermalinkPress={onPermalinkPress}
|
||||||
|
textStyles={textStyles}
|
||||||
|
value={attachment.text}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<AttachmentFields
|
||||||
|
baseTextStyle={baseTextStyle}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
fields={attachment.fields}
|
||||||
|
metadata={metadata}
|
||||||
|
onPermalinkPress={onPermalinkPress}
|
||||||
|
textStyles={textStyles}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<AttachmentFooter
|
||||||
|
icon={attachment.footer_icon}
|
||||||
|
text={attachment.footer}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<AttachmentActions
|
||||||
|
actions={attachment.actions}
|
||||||
|
postId={postId}
|
||||||
|
/>
|
||||||
|
{hasImage &&
|
||||||
|
<AttachmentImage
|
||||||
|
deviceHeight={deviceHeight}
|
||||||
|
deviceWidth={deviceWidth}
|
||||||
|
imageUrl={attachment.image_url}
|
||||||
|
imageMetadata={metadata?.images?.[attachment.image_url]}
|
||||||
|
postId={postId}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme:Theme) => {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||||
|
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||||
|
borderTopColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
marginTop: 5,
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
borderLeftColor: changeOpacity(theme.linkColor, 0.6),
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -5,9 +5,9 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {StyleSheet, View} from 'react-native';
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
import ProgressiveImage from 'app/components/progressive_image';
|
import ProgressiveImage from '@components/progressive_image';
|
||||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||||
import {isGifTooLarge} from 'app/utils/images';
|
import {isGifTooLarge} from '@utils/images';
|
||||||
|
|
||||||
export default class PostAttachmentImage extends React.PureComponent {
|
export default class PostAttachmentImage extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
|
|||||||
|
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
if (!MessageAttachments) {
|
if (!MessageAttachments) {
|
||||||
MessageAttachments = require('app/components/message_attachments').default;
|
MessageAttachments = require('@components/message_attachments').default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ exports[`PostDraft Should render the Archived for deactivatedChannel 1`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`PostDraft Should render the DraftInput 1`] = `
|
exports[`PostDraft Should render the DraftInput 1`] = `
|
||||||
<KeyboardTrackingView>
|
<KeyboardTrackingView
|
||||||
|
inverted={false}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
|||||||
import {Platform} from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||||
|
|
||||||
|
import {DeviceTypes} from '@constants';
|
||||||
import {UPDATE_NATIVE_SCROLLVIEW} from '@constants/post_draft';
|
import {UPDATE_NATIVE_SCROLLVIEW} from '@constants/post_draft';
|
||||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ export default class PostDraft extends PureComponent {
|
|||||||
accessoriesContainerID={accessoriesContainerID}
|
accessoriesContainerID={accessoriesContainerID}
|
||||||
ref={this.keyboardTracker}
|
ref={this.keyboardTracker}
|
||||||
scrollViewNativeID={scrollViewNativeID}
|
scrollViewNativeID={scrollViewNativeID}
|
||||||
|
inverted={DeviceTypes.IS_TABLET}
|
||||||
>
|
>
|
||||||
{draftInput}
|
{draftInput}
|
||||||
</KeyboardTrackingView>
|
</KeyboardTrackingView>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import PasteableTextInput from '@components/pasteable_text_input';
|
|||||||
import {NavigationTypes} from '@constants';
|
import {NavigationTypes} from '@constants';
|
||||||
import DEVICE from '@constants/device';
|
import DEVICE from '@constants/device';
|
||||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||||
|
import {debounce} from '@mm-redux/actions/helpers';
|
||||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||||
import {t} from '@utils/i18n';
|
import {t} from '@utils/i18n';
|
||||||
import {switchKeyboardForCodeBlocks} from '@utils/markdown';
|
import {switchKeyboardForCodeBlocks} from '@utils/markdown';
|
||||||
@@ -86,7 +87,7 @@ export default class PostInput extends PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
changeDraft = (text) => {
|
changeDraft = debounce((text) => {
|
||||||
const {
|
const {
|
||||||
channelId,
|
channelId,
|
||||||
handleCommentDraftChanged,
|
handleCommentDraftChanged,
|
||||||
@@ -99,7 +100,7 @@ export default class PostInput extends PureComponent {
|
|||||||
} else {
|
} else {
|
||||||
handlePostDraftChanged(channelId, text);
|
handlePostDraftChanged(channelId, text);
|
||||||
}
|
}
|
||||||
};
|
}, 200);
|
||||||
|
|
||||||
checkMessageLength = (value) => {
|
checkMessageLength = (value) => {
|
||||||
const {intl} = this.context;
|
const {intl} = this.context;
|
||||||
@@ -163,7 +164,7 @@ export default class PostInput extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleEndEditing = (e) => {
|
handleEndEditing = (e) => {
|
||||||
if (e && e.nativeEvent) {
|
if (e && e.nativeEvent && !DEVICE.IS_TABLET) {
|
||||||
this.changeDraft(e.nativeEvent.text || '');
|
this.changeDraft(e.nativeEvent.text || '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -196,6 +197,9 @@ export default class PostInput extends PureComponent {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
updateInitialValue(value);
|
updateInitialValue(value);
|
||||||
|
if (DEVICE.IS_TABLET) {
|
||||||
|
this.changeDraft(value);
|
||||||
|
}
|
||||||
|
|
||||||
if (inputEventType) {
|
if (inputEventType) {
|
||||||
EventEmitter.emit(inputEventType, value);
|
EventEmitter.emit(inputEventType, value);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Alert} from 'react-native';
|
import {Alert} from 'react-native';
|
||||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||||
|
import TestHelper from 'test/test_helper';
|
||||||
|
|
||||||
|
import Device from '@constants/device';
|
||||||
import Preferences from '@mm-redux/constants/preferences';
|
import Preferences from '@mm-redux/constants/preferences';
|
||||||
|
|
||||||
import PostInput from './post_input';
|
import PostInput from './post_input';
|
||||||
@@ -48,7 +50,7 @@ describe('PostInput', () => {
|
|||||||
expect(instance.changeDraft).not.toBeCalled();
|
expect(instance.changeDraft).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should emit the event and text is save to draft', () => {
|
test('should emit the event and text is save to draft', async () => {
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
<PostInput {...baseProps}/>,
|
<PostInput {...baseProps}/>,
|
||||||
);
|
);
|
||||||
@@ -58,6 +60,7 @@ describe('PostInput', () => {
|
|||||||
|
|
||||||
instance.setValue(value);
|
instance.setValue(value);
|
||||||
instance.handleAppStateChange('background');
|
instance.handleAppStateChange('background');
|
||||||
|
await TestHelper.wait(200);
|
||||||
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledWith(baseProps.channelId, value);
|
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledWith(baseProps.channelId, value);
|
||||||
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledTimes(1);
|
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -76,5 +79,36 @@ describe('PostInput', () => {
|
|||||||
expect(Alert.alert).toBeCalled();
|
expect(Alert.alert).toBeCalled();
|
||||||
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should save the draft onChangeText for tablets', async () => {
|
||||||
|
Device.IS_TABLET = true;
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<PostInput {...baseProps}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const instance = wrapper.instance();
|
||||||
|
const value = 'some text';
|
||||||
|
|
||||||
|
instance.handleTextChange(value);
|
||||||
|
await TestHelper.wait(200);
|
||||||
|
|
||||||
|
expect(baseProps.handlePostDraftChanged).toBeCalled();
|
||||||
|
expect(baseProps.handlePostDraftChanged).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not save the draft onEndEditing for tablets', async () => {
|
||||||
|
Device.IS_TABLET = true;
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<PostInput {...baseProps}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const instance = wrapper.instance();
|
||||||
|
const value = 'some text';
|
||||||
|
|
||||||
|
instance.handleEndEditing(value);
|
||||||
|
await TestHelper.wait(200);
|
||||||
|
|
||||||
|
expect(baseProps.handlePostDraftChanged).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {Platform, View} from 'react-native';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
|
|
||||||
import {Client4} from '@client/rest';
|
|
||||||
|
|
||||||
import CompassIcon from '@components/compass_icon';
|
|
||||||
import UserStatus from '@components/user_status';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
||||||
|
|
||||||
const STATUS_BUFFER = Platform.select({
|
|
||||||
ios: 3,
|
|
||||||
android: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class ProfilePicture extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
isCurrentUser: PropTypes.bool,
|
|
||||||
size: PropTypes.number,
|
|
||||||
statusSize: PropTypes.number,
|
|
||||||
iconSize: PropTypes.number,
|
|
||||||
user: PropTypes.object,
|
|
||||||
userId: PropTypes.string,
|
|
||||||
showStatus: PropTypes.bool,
|
|
||||||
status: PropTypes.string,
|
|
||||||
edit: PropTypes.bool,
|
|
||||||
imageUri: PropTypes.string,
|
|
||||||
profileImageUri: PropTypes.string,
|
|
||||||
profileImageRemove: PropTypes.bool,
|
|
||||||
theme: PropTypes.object,
|
|
||||||
testID: PropTypes.string,
|
|
||||||
actions: PropTypes.shape({
|
|
||||||
getStatusForId: PropTypes.func.isRequired,
|
|
||||||
setProfileImageUri: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
showStatus: true,
|
|
||||||
size: 128,
|
|
||||||
statusSize: 14,
|
|
||||||
edit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
pictureUrl: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {actions, edit, imageUri, profileImageUri, user, status} = this.props;
|
|
||||||
this.mounted = true;
|
|
||||||
|
|
||||||
if (!status && user) {
|
|
||||||
actions.getStatusForId(user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profileImageUri) {
|
|
||||||
this.setImageURL(profileImageUri);
|
|
||||||
} else if (edit && imageUri) {
|
|
||||||
this.setImageURL(imageUri);
|
|
||||||
} else if (user) {
|
|
||||||
const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update);
|
|
||||||
|
|
||||||
this.setImageURL(uri);
|
|
||||||
this.clearProfileImageUri();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.mounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setImageURL = (pictureUrl) => {
|
|
||||||
if (this.mounted) {
|
|
||||||
this.setState({pictureUrl});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
clearProfileImageUri = () => {
|
|
||||||
if (this.props.isCurrentUser && this.props.profileImageUri !== '') {
|
|
||||||
this.props.actions.setProfileImageUri('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.profileImageRemove !== prevProps.profileImageRemove) {
|
|
||||||
this.setImageURL(null);
|
|
||||||
} else if (this.mounted) {
|
|
||||||
if (this.props.edit && this.props.imageUri && this.props.imageUri !== prevProps.imageUri) {
|
|
||||||
this.setImageURL(this.props.imageUri);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.profileImageUri !== '' && this.props.profileImageUri !== prevProps.profileImageUri) {
|
|
||||||
this.setImageURL(this.props.profileImageUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = prevProps.user ? Client4.getProfilePictureUrl(prevProps.user.id, prevProps.user.last_picture_update) : null;
|
|
||||||
const nextUrl = this.props.user ? Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update) : null;
|
|
||||||
|
|
||||||
if (nextUrl && url !== nextUrl) {
|
|
||||||
// empty function is so that promise unhandled is not triggered in dev mode
|
|
||||||
this.setImageURL(nextUrl);
|
|
||||||
this.clearProfileImageUri();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {edit, showStatus, theme, user, size, testID} = this.props;
|
|
||||||
const {pictureUrl} = this.state;
|
|
||||||
const style = getStyleSheet(theme);
|
|
||||||
|
|
||||||
let statusIcon;
|
|
||||||
let statusStyle;
|
|
||||||
let containerStyle = {
|
|
||||||
width: size + STATUS_BUFFER,
|
|
||||||
height: size + STATUS_BUFFER,
|
|
||||||
};
|
|
||||||
if (edit) {
|
|
||||||
const iconColor = changeOpacity(theme.centerChannelColor, 0.6);
|
|
||||||
statusStyle = {
|
|
||||||
width: this.props.statusSize,
|
|
||||||
height: this.props.statusSize,
|
|
||||||
backgroundColor: theme.centerChannelBg,
|
|
||||||
};
|
|
||||||
statusIcon = (
|
|
||||||
<CompassIcon
|
|
||||||
name='camera-outline'
|
|
||||||
size={this.props.statusSize / 1.7}
|
|
||||||
color={iconColor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (this.props.status && !edit) {
|
|
||||||
statusIcon = (
|
|
||||||
<UserStatus
|
|
||||||
size={this.props.statusSize}
|
|
||||||
status={this.props.status}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = null;
|
|
||||||
let image;
|
|
||||||
if (pictureUrl) {
|
|
||||||
let prefix = '';
|
|
||||||
if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') &&
|
|
||||||
!pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) {
|
|
||||||
prefix = 'file://';
|
|
||||||
}
|
|
||||||
|
|
||||||
source = {
|
|
||||||
uri: `${prefix}${pictureUrl}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
image = (
|
|
||||||
<FastImage
|
|
||||||
key={pictureUrl}
|
|
||||||
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
|
|
||||||
source={source}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
containerStyle = {
|
|
||||||
width: size + (STATUS_BUFFER - 1),
|
|
||||||
height: size + (STATUS_BUFFER - 1),
|
|
||||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
|
||||||
};
|
|
||||||
image = (
|
|
||||||
<CompassIcon
|
|
||||||
name='account-outline'
|
|
||||||
size={this.props.iconSize || this.props.size}
|
|
||||||
style={style.icon}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[style.container, containerStyle]}
|
|
||||||
testID={`${testID}.${user?.id}`}
|
|
||||||
>
|
|
||||||
{image}
|
|
||||||
{(showStatus || edit) && (user && !user.is_bot) &&
|
|
||||||
<View style={[style.statusWrapper, statusStyle, {borderRadius: this.props.statusSize / 2}]}>
|
|
||||||
{statusIcon}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|
||||||
return {
|
|
||||||
container: {
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 80,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
color: changeOpacity(theme.centerChannelColor, 0.48),
|
|
||||||
},
|
|
||||||
statusWrapper: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
overflow: 'hidden',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: theme.centerChannelBg,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
color: theme.centerChannelBg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
216
app/components/profile_picture/profile_picture.tsx
Normal file
216
app/components/profile_picture/profile_picture.tsx
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {Platform, StyleProp, View, ViewProps, ViewStyle} from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
|
import {Client4} from '@client/rest';
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import UserStatus from '@components/user_status';
|
||||||
|
import {useDidUpdate} from '@hooks';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import type {Theme} from '@mm-redux/types/preferences';
|
||||||
|
import type {UserProfile} from '@mm-redux/types/users';
|
||||||
|
|
||||||
|
const STATUS_BUFFER = Platform.select({
|
||||||
|
ios: 3,
|
||||||
|
android: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
type ProfilePictureProps = {
|
||||||
|
actions: {
|
||||||
|
setProfileImageUri: (imageUri?: string) => void;
|
||||||
|
getStatusForId: (id: string) => void;
|
||||||
|
};
|
||||||
|
edit: boolean;
|
||||||
|
iconSize?: number;
|
||||||
|
imageUri?: string;
|
||||||
|
isCurrentUser: boolean;
|
||||||
|
profileImageRemove?: boolean;
|
||||||
|
profileImageUri?: string;
|
||||||
|
showStatus: boolean;
|
||||||
|
size: number;
|
||||||
|
status?: string;
|
||||||
|
statusSize: number;
|
||||||
|
statusStyle?: StyleProp<ViewProps> | any;
|
||||||
|
user?: UserProfile;
|
||||||
|
userId: string;
|
||||||
|
theme: Theme;
|
||||||
|
testID?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 80,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.48),
|
||||||
|
},
|
||||||
|
statusWrapper: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.centerChannelBg,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
color: theme.centerChannelBg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProfilePicture = (props: ProfilePictureProps) => {
|
||||||
|
const [pictureUrl, setPictureUrl] = useState<string|undefined>();
|
||||||
|
const style = getStyleSheet(props.theme);
|
||||||
|
const buffer = STATUS_BUFFER || 0;
|
||||||
|
|
||||||
|
const clearProfileImageUri = () => {
|
||||||
|
if (props.isCurrentUser && props.profileImageUri !== '') {
|
||||||
|
props.actions.setProfileImageUri('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const {edit, imageUri, profileImageUri, user, status} = props;
|
||||||
|
const {getStatusForId} = props.actions;
|
||||||
|
|
||||||
|
if (!status && user) {
|
||||||
|
getStatusForId(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileImageUri) {
|
||||||
|
setPictureUrl(profileImageUri);
|
||||||
|
} else if (edit && imageUri) {
|
||||||
|
setPictureUrl(imageUri);
|
||||||
|
} else if (user) {
|
||||||
|
const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update);
|
||||||
|
|
||||||
|
setPictureUrl(uri);
|
||||||
|
clearProfileImageUri();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
setPictureUrl(undefined);
|
||||||
|
}, [props.profileImageRemove]);
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
const {edit, imageUri} = props;
|
||||||
|
if (edit && imageUri) {
|
||||||
|
setPictureUrl(imageUri);
|
||||||
|
}
|
||||||
|
}, [props.edit, props.imageUri]);
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
const {profileImageUri} = props;
|
||||||
|
if (profileImageUri) {
|
||||||
|
setPictureUrl(profileImageUri);
|
||||||
|
}
|
||||||
|
}, [props.profileImageUri]);
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
const {user} = props;
|
||||||
|
|
||||||
|
const url = user ? Client4.getProfilePictureUrl(user.id, user.last_picture_update) : undefined;
|
||||||
|
if (url !== pictureUrl) {
|
||||||
|
setPictureUrl(url);
|
||||||
|
}
|
||||||
|
}, [props.user]);
|
||||||
|
|
||||||
|
let statusIcon;
|
||||||
|
let statusStyle = props.statusStyle;
|
||||||
|
let containerStyle: StyleProp<ViewStyle> = {
|
||||||
|
width: props.size + buffer,
|
||||||
|
height: props.size + buffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.edit) {
|
||||||
|
const iconColor = changeOpacity(props.theme.centerChannelColor, 0.6);
|
||||||
|
statusStyle = {
|
||||||
|
width: props.statusSize,
|
||||||
|
height: props.statusSize,
|
||||||
|
backgroundColor: props.theme.centerChannelBg,
|
||||||
|
};
|
||||||
|
statusIcon = (
|
||||||
|
<CompassIcon
|
||||||
|
name='camera-outline'
|
||||||
|
size={props.statusSize / 1.7}
|
||||||
|
color={iconColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (props.status && !props.edit) {
|
||||||
|
statusIcon = (
|
||||||
|
<UserStatus
|
||||||
|
size={props.statusSize}
|
||||||
|
status={props.status}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = null;
|
||||||
|
let image;
|
||||||
|
if (pictureUrl) {
|
||||||
|
let prefix = '';
|
||||||
|
if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') &&
|
||||||
|
!pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) {
|
||||||
|
prefix = 'file://';
|
||||||
|
}
|
||||||
|
|
||||||
|
source = {
|
||||||
|
uri: `${prefix}${pictureUrl}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
image = (
|
||||||
|
<FastImage
|
||||||
|
key={pictureUrl}
|
||||||
|
style={{width: props.size, height: props.size, borderRadius: props.size / 2}}
|
||||||
|
source={source}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
containerStyle = {
|
||||||
|
width: props.size + (buffer - 1),
|
||||||
|
height: props.size + (buffer - 1),
|
||||||
|
backgroundColor: changeOpacity(props.theme.centerChannelColor, 0.08),
|
||||||
|
};
|
||||||
|
image = (
|
||||||
|
<CompassIcon
|
||||||
|
name='account-outline'
|
||||||
|
size={props.iconSize || props.size}
|
||||||
|
style={style.icon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[style.container, containerStyle]}
|
||||||
|
testID={`${props.testID}.${props.user?.id}`}
|
||||||
|
>
|
||||||
|
{image}
|
||||||
|
{(props.showStatus || props.edit) && (!props.user?.is_bot) &&
|
||||||
|
<View style={[style.statusWrapper, statusStyle, {borderRadius: props.statusSize / 2}]}>
|
||||||
|
{statusIcon}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfilePicture.defaultProps = {
|
||||||
|
showStatus: true,
|
||||||
|
size: 128,
|
||||||
|
statusSize: 14,
|
||||||
|
edit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePicture;
|
||||||
@@ -8,13 +8,13 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
import Emoji from 'app/components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {hapticFeedback} from 'app/utils/general';
|
import {hapticFeedback} from '@utils/general';
|
||||||
import {
|
import {
|
||||||
LARGE_CONTAINER_SIZE,
|
LARGE_CONTAINER_SIZE,
|
||||||
LARGE_ICON_SIZE,
|
LARGE_ICON_SIZE,
|
||||||
} from 'app/constants/reaction_picker';
|
} from '@constants/reaction_picker';
|
||||||
|
|
||||||
export default class ReactionButton extends PureComponent {
|
export default class ReactionButton extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
import Emoji from 'app/components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
export default class Reaction extends PureComponent {
|
export default class Reaction extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|||||||
@@ -37,12 +37,16 @@ exports[`ChannelItem should match snapshot 1`] = `
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -87,6 +91,7 @@ exports[`ChannelItem should match snapshot 1`] = `
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -153,12 +158,16 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={true}
|
isActive={true}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -203,6 +212,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -213,7 +223,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
|||||||
}
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.display_name"
|
testID="main.sidebar.channels_list.list.channel_item.display_name"
|
||||||
>
|
>
|
||||||
{displayname} (you)
|
display_name
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -268,12 +278,16 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={true}
|
isActive={true}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -318,6 +332,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -383,12 +398,16 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={true}
|
isActive={true}
|
||||||
isArchived={true}
|
isArchived={true}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -433,6 +452,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -487,12 +507,16 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -537,6 +561,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -592,12 +617,16 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -642,6 +671,7 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -697,12 +727,16 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = `
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -747,6 +781,7 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = `
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -806,12 +841,16 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
|
|||||||
hasDraft={true}
|
hasDraft={true}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -856,6 +895,7 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
@@ -913,12 +953,16 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
|
|||||||
hasDraft={false}
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={false}
|
isInfo={false}
|
||||||
isUnread={true}
|
isUnread={true}
|
||||||
membersCount={1}
|
membersCount={1}
|
||||||
size={16}
|
size={24}
|
||||||
status="online"
|
statusStyle={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#145dbf",
|
||||||
|
"borderColor": "transparent",
|
||||||
|
}
|
||||||
|
}
|
||||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -963,6 +1007,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
|
|||||||
"fontFamily": "Open Sans",
|
"fontFamily": "Open Sans",
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 24,
|
"lineHeight": 24,
|
||||||
|
"marginLeft": 13,
|
||||||
"maxWidth": "80%",
|
"maxWidth": "80%",
|
||||||
"paddingRight": 10,
|
"paddingRight": 10,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
|
|
||||||
|
import Badge from '@components/badge';
|
||||||
|
import ChannelIcon from '@components/channel_icon';
|
||||||
import {General} from '@mm-redux/constants';
|
import {General} from '@mm-redux/constants';
|
||||||
import Badge from 'app/components/badge';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import ChannelIcon from 'app/components/channel_icon';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {preventDoubleTap} from 'app/utils/tap';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
|
||||||
|
|
||||||
export default class ChannelItem extends PureComponent {
|
export default class ChannelItem extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -33,10 +33,10 @@ export default class ChannelItem extends PureComponent {
|
|||||||
onSelectChannel: PropTypes.func.isRequired,
|
onSelectChannel: PropTypes.func.isRequired,
|
||||||
shouldHideChannel: PropTypes.bool,
|
shouldHideChannel: PropTypes.bool,
|
||||||
showUnreadForMsgs: PropTypes.bool.isRequired,
|
showUnreadForMsgs: PropTypes.bool.isRequired,
|
||||||
|
teammateId: PropTypes.string,
|
||||||
theme: PropTypes.object.isRequired,
|
theme: PropTypes.object.isRequired,
|
||||||
unreadMsgs: PropTypes.number.isRequired,
|
unreadMsgs: PropTypes.number.isRequired,
|
||||||
isSearchResult: PropTypes.bool,
|
isSearchResult: PropTypes.bool,
|
||||||
isBot: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -77,7 +77,7 @@ export default class ChannelItem extends PureComponent {
|
|||||||
theme,
|
theme,
|
||||||
isSearchResult,
|
isSearchResult,
|
||||||
channel,
|
channel,
|
||||||
isBot,
|
teammateId,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Only ever show an archived channel if it's the currently viewed channel.
|
// Only ever show an archived channel if it's the currently viewed channel.
|
||||||
@@ -103,7 +103,7 @@ export default class ChannelItem extends PureComponent {
|
|||||||
if (isSearchResult) {
|
if (isSearchResult) {
|
||||||
isCurrenUser = channel.id === currentUserId;
|
isCurrenUser = channel.id === currentUserId;
|
||||||
} else {
|
} else {
|
||||||
isCurrenUser = channel.teammate_id === currentUserId;
|
isCurrenUser = teammateId === currentUserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isCurrenUser) {
|
if (isCurrenUser) {
|
||||||
@@ -161,13 +161,13 @@ export default class ChannelItem extends PureComponent {
|
|||||||
isUnread={isUnread}
|
isUnread={isUnread}
|
||||||
hasDraft={hasDraft && channelId !== currentChannelId}
|
hasDraft={hasDraft && channelId !== currentChannelId}
|
||||||
membersCount={displayName.split(',').length}
|
membersCount={displayName.split(',').length}
|
||||||
size={16}
|
statusStyle={{backgroundColor: theme.sidebarBg, borderColor: 'transparent'}}
|
||||||
status={channel.status}
|
size={24}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
type={channel.type}
|
type={channel.type}
|
||||||
isArchived={isArchived}
|
isArchived={isArchived}
|
||||||
isBot={isBot}
|
|
||||||
testID={`${testID}.channel_icon`}
|
testID={`${testID}.channel_icon`}
|
||||||
|
userId={teammateId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -231,6 +231,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
paddingRight: 10,
|
paddingRight: 10,
|
||||||
|
marginLeft: 13,
|
||||||
maxWidth: '80%',
|
maxWidth: '80%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
|||||||
@@ -30,16 +30,14 @@ function makeMapStateToProps() {
|
|||||||
const channelDraft = getDraftForChannel(state, channel.id);
|
const channelDraft = getDraftForChannel(state, channel.id);
|
||||||
|
|
||||||
let displayName = channel.display_name;
|
let displayName = channel.display_name;
|
||||||
let isBot = false;
|
|
||||||
let isGuest = false;
|
let isGuest = false;
|
||||||
let isArchived = channel.delete_at > 0;
|
let isArchived = channel.delete_at > 0;
|
||||||
|
let teammateId;
|
||||||
|
|
||||||
if (channel.type === General.DM_CHANNEL) {
|
if (channel.type === General.DM_CHANNEL) {
|
||||||
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||||
const teammate = getUser(state, teammateId);
|
const teammate = getUser(state, teammateId);
|
||||||
|
|
||||||
isBot = Boolean(ownProps.isSearchResult ? channel.isBot : teammate?.is_bot); //eslint-disable-line camelcase
|
|
||||||
|
|
||||||
if (teammate) {
|
if (teammate) {
|
||||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||||
displayName = displayUsername(teammate, teammateNameDisplay, false);
|
displayName = displayUsername(teammate, teammateNameDisplay, false);
|
||||||
@@ -78,13 +76,13 @@ function makeMapStateToProps() {
|
|||||||
displayName,
|
displayName,
|
||||||
hasDraft: Boolean(channelDraft.draft.trim() || channelDraft?.files?.length),
|
hasDraft: Boolean(channelDraft.draft.trim() || channelDraft?.files?.length),
|
||||||
isArchived,
|
isArchived,
|
||||||
isBot,
|
|
||||||
isChannelMuted: isChannelMuted(member),
|
isChannelMuted: isChannelMuted(member),
|
||||||
isGuest,
|
isGuest,
|
||||||
isManualUnread: isManuallyUnread(state, ownProps.channelId),
|
isManualUnread: isManuallyUnread(state, ownProps.channelId),
|
||||||
mentions: member ? member.mention_count : 0,
|
mentions: member ? member.mention_count : 0,
|
||||||
shouldHideChannel,
|
shouldHideChannel,
|
||||||
showUnreadForMsgs,
|
showUnreadForMsgs,
|
||||||
|
teammateId,
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
unreadMsgs,
|
unreadMsgs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class FilteredList extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
buildCurrentDMSForSearch = (props, term) => {
|
buildCurrentDMSForSearch = (props, term) => {
|
||||||
const {channels, teammateNameDisplay, profiles, statuses, pastDirectMessages, groupChannelMemberDetails} = props;
|
const {channels, currentUserId, teammateNameDisplay, profiles, pastDirectMessages, groupChannelMemberDetails} = props;
|
||||||
const {favoriteChannels} = channels;
|
const {favoriteChannels} = channels;
|
||||||
|
|
||||||
const favoriteDms = favoriteChannels.filter((c) => {
|
const favoriteDms = favoriteChannels.filter((c) => {
|
||||||
@@ -206,7 +206,6 @@ class FilteredList extends Component {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
status: statuses[u.id],
|
|
||||||
display_name: displayName,
|
display_name: displayName,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
@@ -215,10 +214,7 @@ class FilteredList extends Component {
|
|||||||
nickname: u.nickname,
|
nickname: u.nickname,
|
||||||
fullname: `${u.first_name} ${u.last_name}`,
|
fullname: `${u.first_name} ${u.last_name}`,
|
||||||
delete_at: u.delete_at,
|
delete_at: u.delete_at,
|
||||||
isBot: u.is_bot,
|
name: `${currentUserId}__${u.id}`,
|
||||||
|
|
||||||
// need name key for DM's as we use it for sortChannelsByDisplayName with same display_name
|
|
||||||
name: displayName,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +229,7 @@ class FilteredList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildMembersForSearch = (props, term) => {
|
buildMembersForSearch = (props, term) => {
|
||||||
const {channels, currentUserId, teammateNameDisplay, profiles, teamProfiles, statuses, pastDirectMessages, restrictDms} = props;
|
const {channels, currentUserId, teammateNameDisplay, profiles, teamProfiles, pastDirectMessages, restrictDms} = props;
|
||||||
const {favoriteChannels, unreadChannels} = channels;
|
const {favoriteChannels, unreadChannels} = channels;
|
||||||
|
|
||||||
const favoriteAndUnreadDms = [...favoriteChannels, ...unreadChannels].filter((c) => {
|
const favoriteAndUnreadDms = [...favoriteChannels, ...unreadChannels].filter((c) => {
|
||||||
@@ -251,17 +247,15 @@ class FilteredList extends Component {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
status: statuses[u.id],
|
|
||||||
display_name: displayName,
|
display_name: displayName,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
name: displayName,
|
name: `${currentUserId}__${u.id}`,
|
||||||
type: General.DM_CHANNEL,
|
type: General.DM_CHANNEL,
|
||||||
fake: true,
|
fake: true,
|
||||||
nickname: u.nickname,
|
nickname: u.nickname,
|
||||||
fullname: `${u.first_name} ${u.last_name}`,
|
fullname: `${u.first_name} ${u.last_name}`,
|
||||||
delete_at: u.delete_at,
|
delete_at: u.delete_at,
|
||||||
isBot: u.is_bot,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports[`ChannelsList List should match snapshot 1`] = `
|
|||||||
onEndReachedThreshold={2}
|
onEndReachedThreshold={2}
|
||||||
onScrollBeginDrag={[Function]}
|
onScrollBeginDrag={[Function]}
|
||||||
onViewableItemsChanged={[Function]}
|
onViewableItemsChanged={[Function]}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={false}
|
||||||
renderItem={[Function]}
|
renderItem={[Function]}
|
||||||
renderSectionHeader={[Function]}
|
renderSectionHeader={[Function]}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ export default class List extends PureComponent {
|
|||||||
ref={this.setListRef}
|
ref={this.setListRef}
|
||||||
sections={sections}
|
sections={sections}
|
||||||
contentContainerStyle={{paddingBottom}}
|
contentContainerStyle={{paddingBottom}}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={Platform.OS === 'android'}
|
||||||
renderItem={this.renderItem}
|
renderItem={this.renderItem}
|
||||||
renderSectionHeader={this.renderSectionHeader}
|
renderSectionHeader={this.renderSectionHeader}
|
||||||
keyboardShouldPersistTaps={'always'}
|
keyboardShouldPersistTaps={'always'}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ exports[`UserStatus should match snapshot, should default to offline status 1`]
|
|||||||
name="circle-outline"
|
name="circle-outline"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"color": "rgba(61,60,64,0.3)",
|
"color": "rgba(184,184,184,0.64)",
|
||||||
"fontSize": 32,
|
"fontSize": 32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {PureComponent} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import {General} from '@mm-redux/constants';
|
|
||||||
|
|
||||||
import CompassIcon from '@components/compass_icon';
|
|
||||||
import {changeOpacity} from '@utils/theme';
|
|
||||||
|
|
||||||
export default class UserStatus extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
isAvatar: PropTypes.bool,
|
|
||||||
size: PropTypes.number,
|
|
||||||
status: PropTypes.string,
|
|
||||||
theme: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
size: 6,
|
|
||||||
status: General.OFFLINE,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {size, status, theme} = this.props;
|
|
||||||
|
|
||||||
let iconName;
|
|
||||||
let iconColor;
|
|
||||||
switch (status) {
|
|
||||||
case General.AWAY:
|
|
||||||
iconName = 'clock';
|
|
||||||
iconColor = theme.awayIndicator;
|
|
||||||
break;
|
|
||||||
case General.DND:
|
|
||||||
iconName = 'minus-circle';
|
|
||||||
iconColor = theme.dndIndicator;
|
|
||||||
break;
|
|
||||||
case General.ONLINE:
|
|
||||||
iconName = 'check-circle';
|
|
||||||
iconColor = theme.onlineIndicator;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
iconName = 'circle-outline';
|
|
||||||
iconColor = changeOpacity(theme.centerChannelColor, 0.3);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CompassIcon
|
|
||||||
name={iconName}
|
|
||||||
style={{fontSize: size, color: iconColor}}
|
|
||||||
testID={`user_status.icon.${status}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
app/components/user_status/user_status.tsx
Normal file
54
app/components/user_status/user_status.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {General} from '@mm-redux/constants';
|
||||||
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
|
||||||
|
import type {Theme} from '@mm-redux/types/preferences';
|
||||||
|
|
||||||
|
type UserStatusProps = {
|
||||||
|
size: number;
|
||||||
|
status: string;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserStatus = ({size, status, theme}: UserStatusProps) => {
|
||||||
|
let iconName;
|
||||||
|
let iconColor;
|
||||||
|
switch (status) {
|
||||||
|
case General.AWAY:
|
||||||
|
iconName = 'clock';
|
||||||
|
iconColor = theme.awayIndicator;
|
||||||
|
break;
|
||||||
|
case General.DND:
|
||||||
|
iconName = 'minus-circle';
|
||||||
|
iconColor = theme.dndIndicator;
|
||||||
|
break;
|
||||||
|
case General.ONLINE:
|
||||||
|
iconName = 'check-circle';
|
||||||
|
iconColor = theme.onlineIndicator;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
iconName = 'circle-outline';
|
||||||
|
iconColor = changeOpacity('#B8B8B8', 0.64);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CompassIcon
|
||||||
|
name={iconName}
|
||||||
|
style={{fontSize: size, color: iconColor}}
|
||||||
|
testID={`user_status.icon.${status}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UserStatus.defaultProps = {
|
||||||
|
size: 6,
|
||||||
|
status: General.OFFLINE,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserStatus;
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
export const ATTACHMENT_DOWNLOAD = 'attachment_download';
|
export const ATTACHMENT_DOWNLOAD = 'attachment_download';
|
||||||
export const MAX_ATTACHMENT_FOOTER_LENGTH = 300;
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`,
|
DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`,
|
||||||
IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`,
|
IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`,
|
||||||
IS_IPHONE_WITH_INSETS: isPhoneWithInsets,
|
IS_IPHONE_WITH_INSETS: isPhoneWithInsets,
|
||||||
IS_TABLET: DeviceInfo.isTablet(),
|
IS_TABLET: isTablet,
|
||||||
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
|
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
|
||||||
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',
|
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',
|
||||||
TABLET_WIDTH: 250,
|
TABLET_WIDTH: 250,
|
||||||
|
|||||||
18
app/hooks/did_update.ts
Normal file
18
app/hooks/did_update.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useRef, useEffect, EffectCallback, DependencyList} from 'react';
|
||||||
|
|
||||||
|
function useDidUpdate(callback: EffectCallback, deps?: DependencyList) {
|
||||||
|
const hasMount = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMount.current) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
hasMount.current = true;
|
||||||
|
}
|
||||||
|
}, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDidUpdate;
|
||||||
4
app/hooks/index.ts
Normal file
4
app/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export {default as useDidUpdate} from './did_update';
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {AppsTypes} from '@mm-redux/action_types';
|
import {AppsTypes} from '@mm-redux/action_types';
|
||||||
import {Client4} from '@client/rest';
|
import {Client4} from '@client/rest';
|
||||||
|
|
||||||
import {ActionFunc} from '@mm-redux/types/actions';
|
import {ActionFunc, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||||
|
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||||
|
|
||||||
import {bindClientFunc} from './helpers';
|
import {bindClientFunc} from './helpers';
|
||||||
|
|
||||||
export function fetchAppBindings(userID: string, channelID: string): ActionFunc {
|
export function fetchAppBindings(userID: string, channelID: string): ActionFunc {
|
||||||
return bindClientFunc({
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
clientFunc: () => Client4.getAppsBindings(userID, channelID),
|
const channel = getChannel(getState(), channelID);
|
||||||
onSuccess: AppsTypes.RECEIVED_APP_BINDINGS,
|
const teamID = channel?.team_id || '';
|
||||||
});
|
|
||||||
|
return dispatch(bindClientFunc({
|
||||||
|
clientFunc: () => Client4.getAppsBindings(userID, channelID, teamID),
|
||||||
|
onSuccess: AppsTypes.RECEIVED_APP_BINDINGS,
|
||||||
|
}));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,6 @@ export function getProfiles(page = 0, perPage: number = General.PROFILE_CHUNK_SI
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.getProfiles(page, perPage, options);
|
profiles = await Client4.getProfiles(page, perPage, options);
|
||||||
removeUserFromList(currentUserId, profiles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -251,7 +250,7 @@ export function getProfiles(page = 0, perPage: number = General.PROFILE_CHUNK_SI
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: profiles,
|
data: removeUserFromList(currentUserId, [...profiles]),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {data: profiles};
|
return {data: profiles};
|
||||||
@@ -307,7 +306,6 @@ export function getProfilesByIds(userIds: Array<string>, options?: any): ActionF
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.getProfilesByIds(userIds, options);
|
profiles = await Client4.getProfilesByIds(userIds, options);
|
||||||
removeUserFromList(currentUserId, profiles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -316,7 +314,7 @@ export function getProfilesByIds(userIds: Array<string>, options?: any): ActionF
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: profiles,
|
data: removeUserFromList(currentUserId, [...profiles]),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {data: profiles};
|
return {data: profiles};
|
||||||
@@ -330,7 +328,6 @@ export function getProfilesByUsernames(usernames: Array<string>): ActionFunc {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.getProfilesByUsernames(usernames);
|
profiles = await Client4.getProfilesByUsernames(usernames);
|
||||||
removeUserFromList(currentUserId, profiles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@@ -339,7 +336,7 @@ export function getProfilesByUsernames(usernames: Array<string>): ActionFunc {
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: profiles,
|
data: removeUserFromList(currentUserId, [...profiles]),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {data: profiles};
|
return {data: profiles};
|
||||||
|
|||||||
30
app/mm-redux/types/message_attachments.ts
Normal file
30
app/mm-redux/types/message_attachments.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {PostAction} from './integration_actions';
|
||||||
|
|
||||||
|
export type MessageAttachment = {
|
||||||
|
id: number;
|
||||||
|
fallback: string;
|
||||||
|
color: string;
|
||||||
|
pretext: string;
|
||||||
|
author_name: string;
|
||||||
|
author_link: string;
|
||||||
|
author_icon: string;
|
||||||
|
title: string;
|
||||||
|
title_link: string;
|
||||||
|
text: string;
|
||||||
|
fields: MessageAttachmentField[];
|
||||||
|
image_url: string;
|
||||||
|
thumb_url: string;
|
||||||
|
footer: string;
|
||||||
|
footer_icon: string;
|
||||||
|
timestamp: number | string;
|
||||||
|
actions?: PostAction[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessageAttachmentField = {
|
||||||
|
title: string;
|
||||||
|
value: any;
|
||||||
|
short: boolean;
|
||||||
|
}
|
||||||
@@ -18,11 +18,12 @@ import {dismissModal} from 'app/actions/navigation';
|
|||||||
|
|
||||||
import DialogIntroductionText from './dialog_introduction_text';
|
import DialogIntroductionText from './dialog_introduction_text';
|
||||||
import {Theme} from '@mm-redux/types/preferences';
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
import {AppCallRequest, AppCallResponse, AppField, AppForm, AppFormValue, AppFormValues, AppLookupResponse, AppSelectOption, FormResponseData} from '@mm-redux/types/apps';
|
import {AppCallRequest, AppField, AppForm, AppFormValue, AppFormValues, AppLookupResponse, AppSelectOption, FormResponseData} from '@mm-redux/types/apps';
|
||||||
import {DialogElement} from '@mm-redux/types/integrations';
|
import {DialogElement} from '@mm-redux/types/integrations';
|
||||||
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||||
import AppsFormField from './apps_form_field';
|
import AppsFormField from './apps_form_field';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
|
import {DoAppCallResult} from 'types/actions/apps';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
call: AppCallRequest;
|
call: AppCallRequest;
|
||||||
@@ -32,9 +33,9 @@ export type Props = {
|
|||||||
values: {
|
values: {
|
||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
}) => Promise<{data?: AppCallResponse<FormResponseData>, error?: AppCallResponse<FormResponseData>}>;
|
}) => Promise<DoAppCallResult<FormResponseData>>;
|
||||||
performLookupCall: (field: AppField, values: AppFormValues, userInput: string) => Promise<{data?: AppCallResponse<AppLookupResponse>, error?: AppCallResponse<AppLookupResponse>}>;
|
performLookupCall: (field: AppField, values: AppFormValues, userInput: string) => Promise<DoAppCallResult<AppLookupResponse>>;
|
||||||
refreshOnSelect: (field: AppField, values: AppFormValues, value: AppFormValue) => Promise<{data?: AppCallResponse<FormResponseData>, error?: AppCallResponse<FormResponseData>}>;
|
refreshOnSelect: (field: AppField, values: AppFormValues, value: AppFormValue) => Promise<DoAppCallResult<FormResponseData>>;
|
||||||
};
|
};
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
componentId: string;
|
componentId: string;
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import React, {PureComponent} from 'react';
|
|||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
|
|
||||||
import {Theme} from '@mm-redux/types/preferences';
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
import {AppCallResponse, AppCallRequest, AppField, AppForm, AppFormValues, FormResponseData, AppCallType, AppLookupResponse} from '@mm-redux/types/apps';
|
import {AppCallResponse, AppCallRequest, AppField, AppForm, AppFormValues, FormResponseData, AppLookupResponse} from '@mm-redux/types/apps';
|
||||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||||
import AppsFormComponent from './apps_form_component';
|
import AppsFormComponent from './apps_form_component';
|
||||||
import {makeCallErrorResponse} from '@utils/apps';
|
import {makeCallErrorResponse} from '@utils/apps';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
import {DoAppCall, DoAppCallResult, PostEphemeralCallResponseForContext} from 'types/actions/apps';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
form?: AppForm;
|
form?: AppForm;
|
||||||
call?: AppCallRequest;
|
call?: AppCallRequest;
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse<any>, error?: AppCallResponse<any>}>;
|
doAppCall: DoAppCall<any>;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForContext: PostEphemeralCallResponseForContext;
|
||||||
};
|
};
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
componentId: string;
|
componentId: string;
|
||||||
@@ -81,7 +81,7 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||||||
switch (callResp.type) {
|
switch (callResp.type) {
|
||||||
case AppCallResponseTypes.OK:
|
case AppCallResponseTypes.OK:
|
||||||
if (callResp.markdown) {
|
if (callResp.markdown) {
|
||||||
this.props.actions.sendEphemeralPost(callResp.markdown, call.context.channel_id, call.context.root_id || call.context.post_id, callResp.app_metadata?.bot_user_id);
|
this.props.actions.postEphemeralCallResponseForContext(callResp, callResp.markdown, call.context);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AppCallResponseTypes.FORM:
|
case AppCallResponseTypes.FORM:
|
||||||
@@ -102,7 +102,7 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshOnSelect = async (field: AppField, values: AppFormValues): Promise<{data?: AppCallResponse<FormResponseData>, error?: AppCallResponse<FormResponseData>}> => {
|
refreshOnSelect = async (field: AppField, values: AppFormValues): Promise<DoAppCallResult<FormResponseData>> => {
|
||||||
const intl = this.context.intl;
|
const intl = this.context.intl;
|
||||||
const makeErrorMsg = (message: string) => intl.formatMessage(
|
const makeErrorMsg = (message: string) => intl.formatMessage(
|
||||||
{
|
{
|
||||||
@@ -171,7 +171,7 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
performLookupCall = async (field: AppField, values: AppFormValues, userInput: string): Promise<{data?: AppCallResponse<AppLookupResponse>, error?: AppCallResponse<AppLookupResponse>}> => {
|
performLookupCall = async (field: AppField, values: AppFormValues, userInput: string): Promise<DoAppCallResult<AppLookupResponse>> => {
|
||||||
const intl = this.context.intl;
|
const intl = this.context.intl;
|
||||||
const makeErrorMsg = (message: string) => intl.formatMessage(
|
const makeErrorMsg = (message: string) => intl.formatMessage(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
|||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||||
import {doAppCall} from '@actions/apps';
|
import {doAppCall, postEphemeralCallResponseForContext} from '@actions/apps';
|
||||||
|
|
||||||
import {AppCallResponse, AppCallRequest, AppCallType} from '@mm-redux/types/apps';
|
|
||||||
import {GlobalState} from '@mm-redux/types/store';
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
import {ActionFunc, GenericAction} from '@mm-redux/types/actions';
|
import {ActionFunc, GenericAction} from '@mm-redux/types/actions';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
|
||||||
|
|
||||||
import AppsFormContainer from './apps_form_container';
|
import AppsFormContainer from './apps_form_container';
|
||||||
import {sendEphemeralPost} from '@actions/views/post';
|
import {DoAppCall, PostEphemeralCallResponseForContext} from 'types/actions/apps';
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForContext: PostEphemeralCallResponseForContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
@@ -30,7 +28,7 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
return {
|
return {
|
||||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||||
doAppCall,
|
doAppCall,
|
||||||
sendEphemeralPost,
|
postEphemeralCallResponseForContext,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,11 @@ exports[`channelInfo should match snapshot 1`] = `
|
|||||||
hasGuests={false}
|
hasGuests={false}
|
||||||
header=""
|
header=""
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isGroupConstrained={false}
|
isGroupConstrained={false}
|
||||||
isTeammateGuest={false}
|
isTeammateGuest={false}
|
||||||
memberCount={2}
|
memberCount={2}
|
||||||
onPermalinkPress={[Function]}
|
onPermalinkPress={[Function]}
|
||||||
purpose="Purpose"
|
purpose="Purpose"
|
||||||
status="status"
|
|
||||||
testID="channel_info.header"
|
testID="channel_info.header"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -27,14 +27,13 @@ exports[`channel_info_header should match snapshot 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChannelIcon
|
<ChannelIcon
|
||||||
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={true}
|
isInfo={true}
|
||||||
isUnread={false}
|
isUnread={false}
|
||||||
membersCount={3}
|
membersCount={3}
|
||||||
size={24}
|
size={24}
|
||||||
status="status"
|
|
||||||
testID="channel_info.header.channel_icon"
|
testID="channel_info.header.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -76,6 +75,7 @@ exports[`channel_info_header should match snapshot 1`] = `
|
|||||||
"flex": 1,
|
"flex": 1,
|
||||||
"fontSize": 15,
|
"fontSize": 15,
|
||||||
"fontWeight": "600",
|
"fontWeight": "600",
|
||||||
|
"marginLeft": 13,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testID="channel_info.header.display_name"
|
testID="channel_info.header.display_name"
|
||||||
@@ -334,14 +334,13 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChannelIcon
|
<ChannelIcon
|
||||||
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={true}
|
isInfo={true}
|
||||||
isUnread={false}
|
isUnread={false}
|
||||||
membersCount={3}
|
membersCount={3}
|
||||||
size={24}
|
size={24}
|
||||||
status="status"
|
|
||||||
testID="channel_info.header.channel_icon"
|
testID="channel_info.header.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -383,6 +382,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
|
|||||||
"flex": 1,
|
"flex": 1,
|
||||||
"fontSize": 15,
|
"fontSize": 15,
|
||||||
"fontWeight": "600",
|
"fontWeight": "600",
|
||||||
|
"marginLeft": 13,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testID="channel_info.header.display_name"
|
testID="channel_info.header.display_name"
|
||||||
@@ -669,14 +669,13 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChannelIcon
|
<ChannelIcon
|
||||||
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={true}
|
isInfo={true}
|
||||||
isUnread={false}
|
isUnread={false}
|
||||||
membersCount={3}
|
membersCount={3}
|
||||||
size={24}
|
size={24}
|
||||||
status="status"
|
|
||||||
testID="channel_info.header.channel_icon"
|
testID="channel_info.header.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -718,6 +717,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
|
|||||||
"flex": 1,
|
"flex": 1,
|
||||||
"fontSize": 15,
|
"fontSize": 15,
|
||||||
"fontWeight": "600",
|
"fontWeight": "600",
|
||||||
|
"marginLeft": 13,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testID="channel_info.header.display_name"
|
testID="channel_info.header.display_name"
|
||||||
@@ -976,14 +976,13 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChannelIcon
|
<ChannelIcon
|
||||||
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={true}
|
isInfo={true}
|
||||||
isUnread={false}
|
isUnread={false}
|
||||||
membersCount={3}
|
membersCount={3}
|
||||||
size={24}
|
size={24}
|
||||||
status="status"
|
|
||||||
testID="channel_info.header.channel_icon"
|
testID="channel_info.header.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -1025,6 +1024,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
|
|||||||
"flex": 1,
|
"flex": 1,
|
||||||
"fontSize": 15,
|
"fontSize": 15,
|
||||||
"fontWeight": "600",
|
"fontWeight": "600",
|
||||||
|
"marginLeft": 13,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testID="channel_info.header.display_name"
|
testID="channel_info.header.display_name"
|
||||||
@@ -1311,14 +1311,13 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChannelIcon
|
<ChannelIcon
|
||||||
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={true}
|
isInfo={true}
|
||||||
isUnread={false}
|
isUnread={false}
|
||||||
membersCount={3}
|
membersCount={3}
|
||||||
size={24}
|
size={24}
|
||||||
status="status"
|
|
||||||
testID="channel_info.header.channel_icon"
|
testID="channel_info.header.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -1360,6 +1359,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
|
|||||||
"flex": 1,
|
"flex": 1,
|
||||||
"fontSize": 15,
|
"fontSize": 15,
|
||||||
"fontWeight": "600",
|
"fontWeight": "600",
|
||||||
|
"marginLeft": 13,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testID="channel_info.header.display_name"
|
testID="channel_info.header.display_name"
|
||||||
@@ -1639,14 +1639,13 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChannelIcon
|
<ChannelIcon
|
||||||
|
hasDraft={false}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
isArchived={false}
|
isArchived={false}
|
||||||
isBot={false}
|
|
||||||
isInfo={true}
|
isInfo={true}
|
||||||
isUnread={false}
|
isUnread={false}
|
||||||
membersCount={3}
|
membersCount={3}
|
||||||
size={24}
|
size={24}
|
||||||
status="status"
|
|
||||||
testID="channel_info.header.channel_icon"
|
testID="channel_info.header.channel_icon"
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
@@ -1688,6 +1687,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
|
|||||||
"flex": 1,
|
"flex": 1,
|
||||||
"fontSize": 15,
|
"fontSize": 15,
|
||||||
"fontWeight": "600",
|
"fontWeight": "600",
|
||||||
|
"marginLeft": 13,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testID="channel_info.header.display_name"
|
testID="channel_info.header.display_name"
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import {intlShape, injectIntl} from 'react-intl';
|
|||||||
import Separator from '@screens/channel_info/separator';
|
import Separator from '@screens/channel_info/separator';
|
||||||
|
|
||||||
import ChannelInfoRow from '../channel_info_row';
|
import ChannelInfoRow from '../channel_info_row';
|
||||||
import {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {AppBinding} from '@mm-redux/types/apps';
|
||||||
import {Theme} from '@mm-redux/types/preferences';
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
import {Channel} from '@mm-redux/types/channels';
|
import {Channel} from '@mm-redux/types/channels';
|
||||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||||
import {dismissModal} from '@actions/navigation';
|
import {dismissModal} from '@actions/navigation';
|
||||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
import {DoAppCall, PostEphemeralCallResponseForChannel} from 'types/actions/apps';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
bindings: AppBinding[];
|
bindings: AppBinding[];
|
||||||
@@ -24,8 +24,8 @@ type Props = {
|
|||||||
intl: typeof intlShape;
|
intl: typeof intlShape;
|
||||||
currentTeamId: string;
|
currentTeamId: string;
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ type OptionProps = {
|
|||||||
intl: typeof intlShape;
|
intl: typeof intlShape;
|
||||||
currentTeamId: string;
|
currentTeamId: string;
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class Option extends React.PureComponent<OptionProps, OptionState> {
|
|||||||
|
|
||||||
onPress = async () => {
|
onPress = async () => {
|
||||||
const {binding, currentChannel, currentTeamId, intl} = this.props;
|
const {binding, currentChannel, currentTeamId, intl} = this.props;
|
||||||
const {doAppCall, sendEphemeralPost} = this.props.actions;
|
const {doAppCall, postEphemeralCallResponseForChannel} = this.props.actions;
|
||||||
|
|
||||||
if (this.state.submitting) {
|
if (this.state.submitting) {
|
||||||
return;
|
return;
|
||||||
@@ -121,11 +121,10 @@ class Option extends React.PureComponent<OptionProps, OptionState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const callResp = res.data!;
|
const callResp = res.data!;
|
||||||
const ephemeral = (message: string) => sendEphemeralPost(message, currentChannel.id, '', callResp.app_metadata?.bot_user_id);
|
|
||||||
switch (callResp.type) {
|
switch (callResp.type) {
|
||||||
case AppCallResponseTypes.OK:
|
case AppCallResponseTypes.OK:
|
||||||
if (callResp.markdown) {
|
if (callResp.markdown) {
|
||||||
ephemeral(callResp.markdown);
|
postEphemeralCallResponseForChannel(callResp, callResp.markdown, currentChannel.id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AppCallResponseTypes.NAVIGATE:
|
case AppCallResponseTypes.NAVIGATE:
|
||||||
|
|||||||
@@ -9,15 +9,13 @@ import {AppBindingLocations} from '@mm-redux/constants/apps';
|
|||||||
import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
|
import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
|
||||||
import {GlobalState} from '@mm-redux/types/store';
|
import {GlobalState} from '@mm-redux/types/store';
|
||||||
import {GenericAction, ActionFunc} from '@mm-redux/types/actions';
|
import {GenericAction, ActionFunc} from '@mm-redux/types/actions';
|
||||||
import {AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {DoAppCall, PostEphemeralCallResponseForChannel} from 'types/actions/apps';
|
||||||
|
|
||||||
import {appsEnabled} from '@utils/apps';
|
import {appsEnabled} from '@utils/apps';
|
||||||
import {doAppCall} from '@actions/apps';
|
import {doAppCall, postEphemeralCallResponseForChannel} from '@actions/apps';
|
||||||
|
|
||||||
import Bindings from './bindings';
|
import Bindings from './bindings';
|
||||||
import {sendEphemeralPost} from '@actions/views/post';
|
|
||||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const apps = appsEnabled(state);
|
const apps = appsEnabled(state);
|
||||||
@@ -33,15 +31,15 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||||
doAppCall,
|
doAppCall,
|
||||||
sendEphemeralPost,
|
postEphemeralCallResponseForChannel,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ export default class ChannelInfo extends PureComponent {
|
|||||||
currentChannelGuestCount: PropTypes.number,
|
currentChannelGuestCount: PropTypes.number,
|
||||||
currentChannelMemberCount: PropTypes.number,
|
currentChannelMemberCount: PropTypes.number,
|
||||||
currentUserId: PropTypes.string,
|
currentUserId: PropTypes.string,
|
||||||
isBot: PropTypes.bool.isRequired,
|
|
||||||
isLandscape: PropTypes.bool.isRequired,
|
|
||||||
isTeammateGuest: PropTypes.bool.isRequired,
|
isTeammateGuest: PropTypes.bool.isRequired,
|
||||||
isDirectMessage: PropTypes.bool.isRequired,
|
isDirectMessage: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string,
|
teammateId: PropTypes.string,
|
||||||
theme: PropTypes.object.isRequired,
|
theme: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,9 +170,8 @@ export default class ChannelInfo extends PureComponent {
|
|||||||
currentChannelCreatorName,
|
currentChannelCreatorName,
|
||||||
currentChannelMemberCount,
|
currentChannelMemberCount,
|
||||||
currentChannelGuestCount,
|
currentChannelGuestCount,
|
||||||
status,
|
teammateId,
|
||||||
theme,
|
theme,
|
||||||
isBot,
|
|
||||||
isTeammateGuest,
|
isTeammateGuest,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -201,11 +198,10 @@ export default class ChannelInfo extends PureComponent {
|
|||||||
memberCount={currentChannelMemberCount}
|
memberCount={currentChannelMemberCount}
|
||||||
onPermalinkPress={this.handlePermalinkPress}
|
onPermalinkPress={this.handlePermalinkPress}
|
||||||
purpose={currentChannel.purpose}
|
purpose={currentChannel.purpose}
|
||||||
status={status}
|
teammateId={teammateId}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
type={currentChannel.type}
|
type={currentChannel.type}
|
||||||
isArchived={channelIsArchived}
|
isArchived={channelIsArchived}
|
||||||
isBot={isBot}
|
|
||||||
isTeammateGuest={isTeammateGuest}
|
isTeammateGuest={isTeammateGuest}
|
||||||
hasGuests={currentChannelGuestCount > 0}
|
hasGuests={currentChannelGuestCount > 0}
|
||||||
isGroupConstrained={currentChannel.group_constrained}
|
isGroupConstrained={currentChannel.group_constrained}
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ describe('channelInfo', () => {
|
|||||||
currentChannelMemberCount: 2,
|
currentChannelMemberCount: 2,
|
||||||
currentChannelGuestCount: 0,
|
currentChannelGuestCount: 0,
|
||||||
currentUserId: '1234',
|
currentUserId: '1234',
|
||||||
status: 'status',
|
|
||||||
theme: Preferences.THEMES.default,
|
theme: Preferences.THEMES.default,
|
||||||
isBot: false,
|
|
||||||
isTeammateGuest: false,
|
isTeammateGuest: false,
|
||||||
isDirectMessage: false,
|
isDirectMessage: false,
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
@@ -12,18 +12,17 @@ import {
|
|||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
import Clipboard from '@react-native-community/clipboard';
|
import Clipboard from '@react-native-community/clipboard';
|
||||||
|
|
||||||
|
import {popToRoot} from '@actions/navigation';
|
||||||
|
import ChannelIcon from '@components/channel_icon';
|
||||||
|
import FormattedDate from '@components/formatted_date';
|
||||||
|
import FormattedText from '@components/formatted_text';
|
||||||
|
import Markdown from '@components/markdown';
|
||||||
import {General} from '@mm-redux/constants';
|
import {General} from '@mm-redux/constants';
|
||||||
|
import BottomSheet from '@utils/bottom_sheet';
|
||||||
import ChannelIcon from 'app/components/channel_icon';
|
import {t} from '@utils/i18n';
|
||||||
import FormattedDate from 'app/components/formatted_date';
|
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||||
import FormattedText from 'app/components/formatted_text';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import Markdown from 'app/components/markdown';
|
|
||||||
import mattermostManaged from 'app/mattermost_managed';
|
import mattermostManaged from 'app/mattermost_managed';
|
||||||
import BottomSheet from 'app/utils/bottom_sheet';
|
|
||||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
|
||||||
import {t} from 'app/utils/i18n';
|
|
||||||
import {popToRoot} from 'app/actions/navigation';
|
|
||||||
|
|
||||||
export default class ChannelInfoHeader extends React.PureComponent {
|
export default class ChannelInfoHeader extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -34,11 +33,10 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||||||
header: PropTypes.string,
|
header: PropTypes.string,
|
||||||
onPermalinkPress: PropTypes.func,
|
onPermalinkPress: PropTypes.func,
|
||||||
purpose: PropTypes.string,
|
purpose: PropTypes.string,
|
||||||
status: PropTypes.string,
|
teammateId: PropTypes.string,
|
||||||
theme: PropTypes.object.isRequired,
|
theme: PropTypes.object.isRequired,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
isArchived: PropTypes.bool.isRequired,
|
isArchived: PropTypes.bool.isRequired,
|
||||||
isBot: PropTypes.bool.isRequired,
|
|
||||||
isTeammateGuest: PropTypes.bool.isRequired,
|
isTeammateGuest: PropTypes.bool.isRequired,
|
||||||
hasGuests: PropTypes.bool.isRequired,
|
hasGuests: PropTypes.bool.isRequired,
|
||||||
isGroupConstrained: PropTypes.bool,
|
isGroupConstrained: PropTypes.bool,
|
||||||
@@ -135,11 +133,10 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||||||
memberCount,
|
memberCount,
|
||||||
onPermalinkPress,
|
onPermalinkPress,
|
||||||
purpose,
|
purpose,
|
||||||
status,
|
teammateId,
|
||||||
theme,
|
theme,
|
||||||
type,
|
type,
|
||||||
isArchived,
|
isArchived,
|
||||||
isBot,
|
|
||||||
isGroupConstrained,
|
isGroupConstrained,
|
||||||
testID,
|
testID,
|
||||||
timeZone,
|
timeZone,
|
||||||
@@ -148,9 +145,10 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
const textStyles = getMarkdownTextStyles(theme);
|
const textStyles = getMarkdownTextStyles(theme);
|
||||||
const blockStyles = getMarkdownBlockStyles(theme);
|
const blockStyles = getMarkdownBlockStyles(theme);
|
||||||
const baseTextStyle = Platform.OS === 'ios' ?
|
const baseTextStyle = Platform.select({
|
||||||
{...style.detail, lineHeight: 20} :
|
ios: {...style.detail, lineHeight: 20},
|
||||||
style.detail;
|
android: style.detail,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={style.container}>
|
<View style={style.container}>
|
||||||
@@ -159,11 +157,10 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||||||
isInfo={true}
|
isInfo={true}
|
||||||
membersCount={memberCount}
|
membersCount={memberCount}
|
||||||
size={24}
|
size={24}
|
||||||
status={status}
|
userId={teammateId}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
type={type}
|
type={type}
|
||||||
isArchived={isArchived}
|
isArchived={isArchived}
|
||||||
isBot={isBot}
|
|
||||||
testID={`${testID}.channel_icon`}
|
testID={`${testID}.channel_icon`}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
@@ -268,6 +265,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: theme.centerChannelColor,
|
color: theme.centerChannelColor,
|
||||||
|
marginLeft: 13,
|
||||||
},
|
},
|
||||||
channelNameContainer: {
|
channelNameContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import {getCustomEmojisInText} from '@mm-redux/actions/emojis';
|
|||||||
import {General} from '@mm-redux/constants';
|
import {General} from '@mm-redux/constants';
|
||||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||||
import {getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
import {getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||||
import {getCurrentUserId, getUser, getStatusForUserId} from '@mm-redux/selectors/entities/users';
|
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||||
import {isLandscape} from '@selectors/device';
|
|
||||||
import {isGuest} from '@utils/users';
|
import {isGuest} from '@utils/users';
|
||||||
|
|
||||||
import ChannelInfo from './channel_info';
|
import ChannelInfo from './channel_info';
|
||||||
@@ -29,17 +28,13 @@ function mapStateToProps(state) {
|
|||||||
let currentChannelGuestCount = (currentChannelStats && currentChannelStats.guest_count) || 0;
|
let currentChannelGuestCount = (currentChannelStats && currentChannelStats.guest_count) || 0;
|
||||||
const currentUserId = getCurrentUserId(state);
|
const currentUserId = getCurrentUserId(state);
|
||||||
|
|
||||||
let status;
|
let teammateId;
|
||||||
let isBot = false;
|
|
||||||
let isTeammateGuest = false;
|
let isTeammateGuest = false;
|
||||||
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
|
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
|
||||||
|
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
const teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name);
|
teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name);
|
||||||
const teammate = getUser(state, teammateId);
|
const teammate = getUser(state, teammateId);
|
||||||
status = getStatusForUserId(state, teammateId);
|
|
||||||
if (teammate && teammate.is_bot) {
|
|
||||||
isBot = true;
|
|
||||||
}
|
|
||||||
if (isGuest(teammate)) {
|
if (isGuest(teammate)) {
|
||||||
isTeammateGuest = true;
|
isTeammateGuest = true;
|
||||||
currentChannelGuestCount = 1;
|
currentChannelGuestCount = 1;
|
||||||
@@ -56,11 +51,9 @@ function mapStateToProps(state) {
|
|||||||
currentChannelGuestCount,
|
currentChannelGuestCount,
|
||||||
currentChannelMemberCount,
|
currentChannelMemberCount,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
isBot,
|
|
||||||
isLandscape: isLandscape(state),
|
|
||||||
isTeammateGuest,
|
isTeammateGuest,
|
||||||
isDirectMessage,
|
isDirectMessage,
|
||||||
status,
|
teammateId,
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export default class EditPost extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const height = Platform.OS === 'android' ? (deviceHeight / 2) - 40 : (deviceHeight / 2);
|
const height = Platform.OS === 'android' ? (deviceHeight / 2) - 40 : (deviceHeight / 2) - 30;
|
||||||
const autocompleteStyles = [
|
const autocompleteStyles = [
|
||||||
style.autocompleteContainer,
|
style.autocompleteContainer,
|
||||||
{flex: autocompleteVisible ? 1 : 0},
|
{flex: autocompleteVisible ? 1 : 0},
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const GalleryViewer = (props: GalleryProps) => {
|
|||||||
const imgHeight = currentFile.height || height;
|
const imgHeight = currentFile.height || height;
|
||||||
const imgWidth = currentFile.width || width;
|
const imgWidth = currentFile.width || width;
|
||||||
const calculatedDimensions = calculateDimensions(imgHeight, imgWidth, width, height);
|
const calculatedDimensions = calculateDimensions(imgHeight, imgWidth, width, height);
|
||||||
const imgCanvas = vec.create(calculatedDimensions.width, calculatedDimensions.height);
|
const imgCanvas = vec.create(calculatedDimensions.width || 0, calculatedDimensions.height || 0);
|
||||||
const minImgVec = vec.min(vec.multiply(-0.5, imgCanvas, sub(scale, 1)), 0);
|
const minImgVec = vec.min(vec.multiply(-0.5, imgCanvas, sub(scale, 1)), 0);
|
||||||
const maxImgVec = vec.max(vec.minus(minImgVec), 0);
|
const maxImgVec = vec.max(vec.minus(minImgVec), 0);
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import {intlShape, injectIntl} from 'react-intl';
|
|||||||
import {isSystemMessage} from '@mm-redux/utils/post_utils';
|
import {isSystemMessage} from '@mm-redux/utils/post_utils';
|
||||||
|
|
||||||
import PostOption from '../post_option';
|
import PostOption from '../post_option';
|
||||||
import {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
import {AppBinding, AppCallResponse} from '@mm-redux/types/apps';
|
||||||
import {Theme} from '@mm-redux/types/preferences';
|
import {Theme} from '@mm-redux/types/preferences';
|
||||||
import {Post} from '@mm-redux/types/posts';
|
import {Post} from '@mm-redux/types/posts';
|
||||||
import {UserProfile} from '@mm-redux/types/users';
|
import {UserProfile} from '@mm-redux/types/users';
|
||||||
import {AppCallResponseTypes, AppCallTypes, AppExpandLevels} from '@mm-redux/constants/apps';
|
import {AppCallResponseTypes, AppCallTypes, AppExpandLevels} from '@mm-redux/constants/apps';
|
||||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||||
import {SendEphemeralPost} from 'types/actions/posts';
|
import {DoAppCall, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
bindings: AppBinding[],
|
bindings: AppBinding[],
|
||||||
@@ -22,12 +22,12 @@ type Props = {
|
|||||||
post: Post,
|
post: Post,
|
||||||
currentUser: UserProfile,
|
currentUser: UserProfile,
|
||||||
teamID: string,
|
teamID: string,
|
||||||
closeWithAnimation: () => void,
|
closeWithAnimation: (cb?: () => void) => void,
|
||||||
appsEnabled: boolean,
|
appsEnabled: boolean,
|
||||||
intl: typeof intlShape,
|
intl: typeof intlShape,
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,18 +69,18 @@ type OptionProps = {
|
|||||||
post: Post,
|
post: Post,
|
||||||
currentUser: UserProfile,
|
currentUser: UserProfile,
|
||||||
teamID: string,
|
teamID: string,
|
||||||
closeWithAnimation: () => void,
|
closeWithAnimation: (cb?: () => void) => void,
|
||||||
intl: typeof intlShape,
|
intl: typeof intlShape,
|
||||||
actions: {
|
actions: {
|
||||||
doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>;
|
doAppCall: DoAppCall;
|
||||||
sendEphemeralPost: SendEphemeralPost;
|
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class Option extends React.PureComponent<OptionProps> {
|
class Option extends React.PureComponent<OptionProps> {
|
||||||
onPress = async () => {
|
onPress = async () => {
|
||||||
const {closeWithAnimation, post, teamID, binding, intl} = this.props;
|
const {closeWithAnimation, post, teamID, binding, intl} = this.props;
|
||||||
const {doAppCall, sendEphemeralPost} = this.props.actions;
|
const {doAppCall, postEphemeralCallResponseForPost} = this.props.actions;
|
||||||
|
|
||||||
if (!binding.call) {
|
if (!binding.call) {
|
||||||
return;
|
return;
|
||||||
@@ -101,47 +101,48 @@ class Option extends React.PureComponent<OptionProps> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
closeWithAnimation();
|
closeWithAnimation(async () => {
|
||||||
const res = await doAppCall(call, AppCallTypes.SUBMIT, intl);
|
const callPromise = doAppCall(call, AppCallTypes.SUBMIT, intl);
|
||||||
if (res.error) {
|
const res = await callPromise;
|
||||||
const errorResponse = res.error;
|
if (res.error) {
|
||||||
const title = intl.formatMessage({
|
const errorResponse = res.error;
|
||||||
id: 'mobile.general.error.title',
|
const title = intl.formatMessage({
|
||||||
defaultMessage: 'Error',
|
id: 'mobile.general.error.title',
|
||||||
});
|
defaultMessage: 'Error',
|
||||||
const errorMessage = errorResponse.error || intl.formatMessage({
|
});
|
||||||
id: 'apps.error.unknown',
|
const errorMessage = errorResponse.error || intl.formatMessage({
|
||||||
defaultMessage: 'Unknown error occurred.',
|
id: 'apps.error.unknown',
|
||||||
});
|
defaultMessage: 'Unknown error occurred.',
|
||||||
Alert.alert(title, errorMessage);
|
});
|
||||||
return;
|
Alert.alert(title, errorMessage);
|
||||||
}
|
return;
|
||||||
|
|
||||||
const callResp = (res as {data: AppCallResponse}).data;
|
|
||||||
const ephemeral = (message: string) => sendEphemeralPost(message, post.channel_id, post.root_id || post.id, callResp.app_metadata?.bot_user_id);
|
|
||||||
switch (callResp.type) {
|
|
||||||
case AppCallResponseTypes.OK:
|
|
||||||
if (callResp.markdown) {
|
|
||||||
ephemeral(callResp.markdown);
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case AppCallResponseTypes.NAVIGATE:
|
const callResp = (res as {data: AppCallResponse}).data;
|
||||||
case AppCallResponseTypes.FORM:
|
switch (callResp.type) {
|
||||||
break;
|
case AppCallResponseTypes.OK:
|
||||||
default: {
|
if (callResp.markdown) {
|
||||||
const title = intl.formatMessage({
|
postEphemeralCallResponseForPost(callResp, callResp.markdown, post);
|
||||||
id: 'mobile.general.error.title',
|
}
|
||||||
defaultMessage: 'Error',
|
break;
|
||||||
});
|
case AppCallResponseTypes.NAVIGATE:
|
||||||
const errMessage = intl.formatMessage({
|
case AppCallResponseTypes.FORM:
|
||||||
id: 'apps.error.responses.unknown_type',
|
break;
|
||||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
default: {
|
||||||
}, {
|
const title = intl.formatMessage({
|
||||||
type: callResp.type,
|
id: 'mobile.general.error.title',
|
||||||
});
|
defaultMessage: 'Error',
|
||||||
Alert.alert(title, errMessage);
|
});
|
||||||
}
|
const errMessage = intl.formatMessage({
|
||||||
}
|
id: 'apps.error.responses.unknown_type',
|
||||||
|
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||||
|
}, {
|
||||||
|
type: callResp.type,
|
||||||
|
});
|
||||||
|
Alert.alert(title, errMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user