Merge beta into master (#1461)
This commit is contained in:
parent
0f3cdda77b
commit
13da75ea44
|
@ -46,7 +46,7 @@ jobs:
|
|||
|
||||
e2e-test:
|
||||
macos:
|
||||
xcode: "10.2.1"
|
||||
xcode: "11.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
|
@ -223,7 +223,7 @@ jobs:
|
|||
|
||||
ios-build:
|
||||
macos:
|
||||
xcode: "10.2.1"
|
||||
xcode: "11.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
|
@ -257,7 +257,8 @@ jobs:
|
|||
- run:
|
||||
name: Update Fastlane
|
||||
command: |
|
||||
sudo bundle install
|
||||
echo "ruby-2.6.4" > ~/.ruby-version
|
||||
bundle install
|
||||
working_directory: ios
|
||||
|
||||
- run:
|
||||
|
@ -319,7 +320,7 @@ jobs:
|
|||
|
||||
ios-testflight:
|
||||
macos:
|
||||
xcode: "10.2.1"
|
||||
xcode: "11.2.1"
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -334,7 +335,8 @@ jobs:
|
|||
- run:
|
||||
name: Update Fastlane
|
||||
command: |
|
||||
sudo bundle install
|
||||
echo "ruby-2.4" > ~/.ruby-version
|
||||
bundle install
|
||||
working_directory: ios
|
||||
|
||||
- run:
|
||||
|
|
|
@ -2,7 +2,7 @@ module.exports = {
|
|||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".ios.js", ".android.js"]
|
||||
"extensions": [".js", ".ios.js", ".android.js", ".native.js", ".tsx"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -68,7 +68,7 @@ Readme will guide you on how to config.
|
|||
## Features
|
||||
| Feature | Status |
|
||||
|--------------------------------------------------------------- |-------- |
|
||||
| Jitsi Integration | ❌ |
|
||||
| Jitsi Integration | ✅ |
|
||||
| Federation (Directory) | ✅ |
|
||||
| Discussions | ❌ |
|
||||
| Threads | ✅ |
|
||||
|
@ -98,11 +98,11 @@ Readme will guide you on how to config.
|
|||
| Unread counter banner on message list | ✅ |
|
||||
| E2E Encryption | ❌ |
|
||||
| Join a Protected Room | ❌ |
|
||||
| Optional Analytics | ❌ |
|
||||
| Optional Analytics | ✅ |
|
||||
| Settings -> About us | ❌ |
|
||||
| Settings -> Contact us | ✅ |
|
||||
| Settings -> Update App Icon | ❌ |
|
||||
| Settings -> Share | ❌ |
|
||||
| Settings -> Share | ✅ |
|
||||
| Accessibility (Medium) | ❌ |
|
||||
| Accessibility (Advanced) | ❌ |
|
||||
| Authentication via Meteor | ❌ |
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default {
|
||||
getModel: () => '',
|
||||
getReadableVersion: () => '',
|
||||
getBundleId: () => ''
|
||||
getBundleId: () => '',
|
||||
isTablet: () => false
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,55 @@
|
|||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
||||
|
||||
lib_deps = []
|
||||
|
||||
create_aar_targets(glob(["libs/*.aar"]))
|
||||
|
||||
create_jar_targets(glob(["libs/*.jar"]))
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "chat.rocket.reactnative",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "chat.rocket.reactnative",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
|
@ -138,7 +138,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "1.20.0"
|
||||
versionName "1.25.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
}
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" android:networkSecurityConfig="@xml/react_native_config" />
|
||||
<application tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">10.0.3.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
|
@ -17,6 +17,7 @@
|
|||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 76 KiB |
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
After Width: | Height: | Size: 492 KiB |
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
|
@ -1,6 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/splashBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/launch_screen" android:scaleType="centerCrop" />
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/launch_screen"
|
||||
android:scaleType="fitCenter"
|
||||
/>
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
|
@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
|||
'CLOSE_SEARCH_HEADER'
|
||||
]);
|
||||
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
|
||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
|
||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
|
||||
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||
export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', 'REMOVE_USER', 'RESET', 'SET_LOADING']);
|
||||
|
|
|
@ -20,6 +20,12 @@ export function appInit() {
|
|||
};
|
||||
}
|
||||
|
||||
export function appInitLocalSettings() {
|
||||
return {
|
||||
type: APP.INIT_LOCAL_SETTINGS
|
||||
};
|
||||
}
|
||||
|
||||
export function setCurrentServer(server) {
|
||||
return {
|
||||
type: types.SET_CURRENT_SERVER,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function loginRequest(credentials) {
|
||||
export function loginRequest(credentials, logoutOnError) {
|
||||
return {
|
||||
type: types.LOGIN.REQUEST,
|
||||
credentials
|
||||
credentials,
|
||||
logoutOnError
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
import { constants } from 'react-native-keycommands';
|
||||
|
||||
import I18n from './i18n';
|
||||
|
||||
const KEY_TYPING = '\t';
|
||||
const KEY_PREFERENCES = 'p';
|
||||
const KEY_SEARCH = 'f';
|
||||
const KEY_PREVIOUS_ROOM = '[';
|
||||
const KEY_NEXT_ROOM = ']';
|
||||
const KEY_NEW_ROOM = __DEV__ ? 'e' : 'n';
|
||||
const KEY_ROOM_ACTIONS = __DEV__ ? 'b' : 'i';
|
||||
const KEY_UPLOAD = 'u';
|
||||
const KEY_REPLY = ';';
|
||||
const KEY_SERVER_SELECTION = __DEV__ ? 'o' : '`';
|
||||
const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n';
|
||||
const KEY_SEND_MESSAGE = '\r';
|
||||
const KEY_SELECT = '123456789';
|
||||
|
||||
export const defaultCommands = [
|
||||
{
|
||||
// Focus messageBox
|
||||
input: KEY_TYPING,
|
||||
modifierFlags: 0,
|
||||
discoverabilityTitle: I18n.t('Type_message')
|
||||
},
|
||||
{
|
||||
// Send message on textInput to current room
|
||||
input: KEY_SEND_MESSAGE,
|
||||
modifierFlags: 0,
|
||||
discoverabilityTitle: I18n.t('Send')
|
||||
}
|
||||
];
|
||||
|
||||
export const keyCommands = [
|
||||
{
|
||||
// Open Preferences Modal
|
||||
input: KEY_PREFERENCES,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Preferences')
|
||||
},
|
||||
{
|
||||
// Focus Room Search
|
||||
input: KEY_SEARCH,
|
||||
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate,
|
||||
discoverabilityTitle: I18n.t('Room_search')
|
||||
},
|
||||
{
|
||||
// Select a room by order using 1-9
|
||||
input: '1...9',
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Room_selection')
|
||||
},
|
||||
{
|
||||
// Change room to next on Rooms List
|
||||
input: KEY_NEXT_ROOM,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Next_room')
|
||||
},
|
||||
{
|
||||
// Change room to previous on Rooms List
|
||||
input: KEY_PREVIOUS_ROOM,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Previous_room')
|
||||
},
|
||||
{
|
||||
// Open New Room Modal
|
||||
input: KEY_NEW_ROOM,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('New_room')
|
||||
},
|
||||
{
|
||||
// Open Room Actions
|
||||
input: KEY_ROOM_ACTIONS,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Room_actions')
|
||||
},
|
||||
{
|
||||
// Upload a file to room
|
||||
input: KEY_UPLOAD,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Upload_room')
|
||||
},
|
||||
{
|
||||
// Search Messages on current room
|
||||
input: KEY_SEARCH,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Search_messages')
|
||||
},
|
||||
{
|
||||
// Scroll messages on current room
|
||||
input: '↑ ↓',
|
||||
modifierFlags: constants.keyModifierAlternate,
|
||||
discoverabilityTitle: I18n.t('Scroll_messages')
|
||||
},
|
||||
{
|
||||
// Scroll up messages on current room
|
||||
input: constants.keyInputUpArrow,
|
||||
modifierFlags: constants.keyModifierAlternate
|
||||
},
|
||||
{
|
||||
// Scroll down messages on current room
|
||||
input: constants.keyInputDownArrow,
|
||||
modifierFlags: constants.keyModifierAlternate
|
||||
},
|
||||
{
|
||||
// Reply latest message with Quote
|
||||
input: KEY_REPLY,
|
||||
modifierFlags: constants.keyModifierCommand,
|
||||
discoverabilityTitle: I18n.t('Reply_latest')
|
||||
},
|
||||
{
|
||||
// Open server dropdown
|
||||
input: KEY_SERVER_SELECTION,
|
||||
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate,
|
||||
discoverabilityTitle: I18n.t('Server_selection')
|
||||
},
|
||||
{
|
||||
// Select a server by order using 1-9
|
||||
input: '1...9',
|
||||
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate,
|
||||
discoverabilityTitle: I18n.t('Server_selection_numbers')
|
||||
},
|
||||
{
|
||||
// Navigate to add new server
|
||||
input: KEY_ADD_SERVER,
|
||||
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate,
|
||||
discoverabilityTitle: I18n.t('Add_server')
|
||||
},
|
||||
// Refers to select rooms on list
|
||||
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
|
||||
input: `${ value }`,
|
||||
modifierFlags: constants.keyModifierCommand
|
||||
}))),
|
||||
// Refers to select servers on list
|
||||
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
|
||||
input: `${ value }`,
|
||||
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate
|
||||
})))
|
||||
];
|
||||
|
||||
export const KEY_COMMAND = 'KEY_COMMAND';
|
||||
|
||||
export const commandHandle = (event, key, flags = []) => {
|
||||
const { input, modifierFlags } = event;
|
||||
let _flags = 0;
|
||||
if (flags.includes('command') && flags.includes('alternate')) {
|
||||
_flags = constants.keyModifierCommand | constants.keyModifierAlternate;
|
||||
} else if (flags.includes('command')) {
|
||||
_flags = constants.keyModifierCommand;
|
||||
} else if (flags.includes('alternate')) {
|
||||
_flags = constants.keyModifierAlternate;
|
||||
}
|
||||
return key.includes(input) && modifierFlags === _flags;
|
||||
};
|
||||
|
||||
export const handleCommandTyping = event => commandHandle(event, KEY_TYPING);
|
||||
|
||||
export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAGE);
|
||||
|
||||
export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']);
|
||||
|
||||
export const handleCommandScroll = event => commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']);
|
||||
|
||||
export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']);
|
||||
|
||||
export const handleCommandSearchMessages = event => commandHandle(event, KEY_SEARCH, ['command']);
|
||||
|
||||
export const handleCommandReplyLatest = event => commandHandle(event, KEY_REPLY, ['command']);
|
||||
|
||||
export const handleCommandSelectServer = event => commandHandle(event, KEY_SELECT, ['command', 'alternate']);
|
||||
|
||||
export const handleCommandShowPreferences = event => commandHandle(event, KEY_PREFERENCES, ['command']);
|
||||
|
||||
export const handleCommandSearching = event => commandHandle(event, KEY_SEARCH, ['command', 'alternate']);
|
||||
|
||||
export const handleCommandSelectRoom = event => commandHandle(event, KEY_SELECT, ['command']);
|
||||
|
||||
export const handleCommandPreviousRoom = event => commandHandle(event, KEY_PREVIOUS_ROOM, ['command']);
|
||||
|
||||
export const handleCommandNextRoom = event => commandHandle(event, KEY_NEXT_ROOM, ['command']);
|
||||
|
||||
export const handleCommandShowNewMessage = event => commandHandle(event, KEY_NEW_ROOM, ['command']);
|
||||
|
||||
export const handleCommandAddNewServer = event => commandHandle(event, KEY_ADD_SERVER, ['command', 'alternate']);
|
||||
|
||||
export const handleCommandOpenServerDropdown = event => commandHandle(event, KEY_SERVER_SELECTION, ['command', 'alternate']);
|
|
@ -1,32 +1,121 @@
|
|||
import { isIOS, isAndroid } from '../utils/deviceInfo';
|
||||
|
||||
export const COLOR_DANGER = '#f5455c';
|
||||
export const COLOR_SUCCESS = '#2de0a5';
|
||||
export const COLOR_PRIMARY = '#1d74f5';
|
||||
export const COLOR_WHITE = '#fff';
|
||||
export const COLOR_BUTTON_PRIMARY = COLOR_PRIMARY;
|
||||
export const COLOR_TITLE = '#0C0D0F';
|
||||
export const COLOR_TEXT = '#2F343D';
|
||||
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
|
||||
export const COLOR_SEPARATOR = '#A7A7AA';
|
||||
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
|
||||
export const COLOR_BACKGROUND_NOTIFICATION = '#f8f8f8';
|
||||
export const COLOR_BORDER = '#e1e5e8';
|
||||
export const COLOR_UNREAD = '#e1e5e8';
|
||||
export const COLOR_TOAST = '#0C0D0F';
|
||||
export const STATUS_COLORS = {
|
||||
online: '#2de0a5',
|
||||
busy: COLOR_DANGER,
|
||||
busy: '#f5455c',
|
||||
away: '#ffd21f',
|
||||
offline: '#cbced1'
|
||||
};
|
||||
|
||||
export const HEADER_BACKGROUND = isIOS ? '#f8f8f8' : '#2F343D';
|
||||
export const HEADER_TITLE = isIOS ? COLOR_TITLE : COLOR_WHITE;
|
||||
export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
|
||||
export const HEADER_TINT = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
|
||||
|
||||
export const SWITCH_TRACK_COLOR = {
|
||||
false: isAndroid ? COLOR_DANGER : null,
|
||||
true: COLOR_SUCCESS
|
||||
false: isAndroid ? '#f5455c' : null,
|
||||
true: '#2de0a5'
|
||||
};
|
||||
|
||||
export const themes = {
|
||||
light: {
|
||||
backgroundColor: '#ffffff',
|
||||
focusedBackground: '#ffffff',
|
||||
chatComponentBackground: '#f3f4f5',
|
||||
auxiliaryBackground: '#efeff4',
|
||||
bannerBackground: '#f1f2f4',
|
||||
titleText: '#0d0e12',
|
||||
bodyText: '#2f343d',
|
||||
backdropColor: '#000000',
|
||||
dangerColor: '#f5455c',
|
||||
successColor: '#2de0a5',
|
||||
borderColor: '#e1e5e8',
|
||||
controlText: '#54585e',
|
||||
auxiliaryText: '#9ca2a8',
|
||||
infoText: '#6d6d72',
|
||||
tintColor: '#1d74f5',
|
||||
auxiliaryTintColor: '#caced1',
|
||||
actionTintColor: '#1d74f5',
|
||||
separatorColor: '#cbcbcc',
|
||||
navbarBackground: '#ffffff',
|
||||
headerBorder: '#B2B2B2',
|
||||
headerBackground: isIOS ? '#f8f8f8' : '#2f343d',
|
||||
headerSecondaryBackground: '#ffffff',
|
||||
headerTintColor: isAndroid ? '#ffffff' : '#1d74f5',
|
||||
headerTitleColor: isAndroid ? '#ffffff' : '#0d0e12',
|
||||
headerSecondaryText: isAndroid ? '#9ca2a8' : '#1d74f5',
|
||||
toastBackground: '#0C0D0F',
|
||||
videoBackground: '#1f2329',
|
||||
favoriteBackground: '#ffbb00',
|
||||
hideBackground: '#54585e',
|
||||
messageboxBackground: '#ffffff',
|
||||
searchboxBackground: '#E6E6E7',
|
||||
buttonBackground: '#414852',
|
||||
buttonText: '#ffffff'
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: '#030b1b',
|
||||
focusedBackground: '#0b182c',
|
||||
chatComponentBackground: '#192132',
|
||||
auxiliaryBackground: '#07101e',
|
||||
bannerBackground: '#0e1f38',
|
||||
titleText: '#FFFFFF',
|
||||
bodyText: '#e8ebed',
|
||||
backdropColor: '#000000',
|
||||
dangerColor: '#f5455c',
|
||||
successColor: '#2de0a5',
|
||||
borderColor: '#0f213d',
|
||||
controlText: '#dadde6',
|
||||
auxiliaryText: '#9297a2',
|
||||
infoText: '#6D6D72',
|
||||
tintColor: '#1d74f5',
|
||||
auxiliaryTintColor: '#cdcdcd',
|
||||
actionTintColor: '#1d74f5',
|
||||
separatorColor: '#2b2b2d',
|
||||
navbarBackground: '#0b182c',
|
||||
headerBorder: '#2F3A4B',
|
||||
headerBackground: '#0b182c',
|
||||
headerSecondaryBackground: '#0b182c',
|
||||
headerTintColor: isAndroid ? '#ffffff' : '#1d74f5',
|
||||
headerTitleColor: '#FFFFFF',
|
||||
headerSecondaryText: isAndroid ? '#9297a2' : '#1d74f5',
|
||||
toastBackground: '#0C0D0F',
|
||||
videoBackground: '#1f2329',
|
||||
favoriteBackground: '#ffbb00',
|
||||
hideBackground: '#54585e',
|
||||
messageboxBackground: '#0b182c',
|
||||
searchboxBackground: '#192d4d',
|
||||
buttonBackground: '#414852',
|
||||
buttonText: '#ffffff'
|
||||
},
|
||||
black: {
|
||||
backgroundColor: '#000000',
|
||||
focusedBackground: '#0d0d0d',
|
||||
chatComponentBackground: '#16181a',
|
||||
auxiliaryBackground: '#080808',
|
||||
bannerBackground: '#1f2329',
|
||||
titleText: '#f9f9f9',
|
||||
bodyText: '#e8ebed',
|
||||
backdropColor: '#000000',
|
||||
dangerColor: '#f5455c',
|
||||
successColor: '#2de0a5',
|
||||
borderColor: '#1f2329',
|
||||
controlText: '#dadde6',
|
||||
auxiliaryText: '#b2b8c6',
|
||||
infoText: '#6d6d72',
|
||||
tintColor: '#1e9bfe',
|
||||
auxiliaryTintColor: '#cdcdcd',
|
||||
actionTintColor: '#1ea1fe',
|
||||
separatorColor: '#272728',
|
||||
navbarBackground: '#0d0d0d',
|
||||
headerBorder: '#323232',
|
||||
headerBackground: '#0d0d0d',
|
||||
headerSecondaryBackground: '#0d0d0d',
|
||||
headerTintColor: isAndroid ? '#ffffff' : '#1e9bfe',
|
||||
headerTitleColor: '#f9f9f9',
|
||||
headerSecondaryText: isAndroid ? '#b2b8c6' : '#1e9bfe',
|
||||
toastBackground: '#0C0D0F',
|
||||
videoBackground: '#1f2329',
|
||||
favoriteBackground: '#ffbb00',
|
||||
hideBackground: '#54585e',
|
||||
messageboxBackground: '#0d0d0d',
|
||||
searchboxBackground: '#1f1f1f',
|
||||
buttonBackground: '#414852',
|
||||
buttonText: '#ffffff'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export const MAX_SIDEBAR_WIDTH = 321;
|
||||
export const MAX_CONTENT_WIDTH = '90%';
|
||||
export const MAX_SCREEN_CONTENT_WIDTH = '45%';
|
||||
export const MIN_WIDTH_SPLIT_LAYOUT = 700;
|
|
@ -1,12 +1,40 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
indicator: {
|
||||
padding: 10
|
||||
padding: 16,
|
||||
flex: 1
|
||||
},
|
||||
absolute: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
const RCActivityIndicator = () => <ActivityIndicator style={styles.indicator} />;
|
||||
const RCActivityIndicator = ({ theme, absolute, ...props }) => (
|
||||
<ActivityIndicator
|
||||
style={[styles.indicator, absolute && styles.absolute]}
|
||||
color={themes[theme].auxiliaryText}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
RCActivityIndicator.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
absolute: PropTypes.bool,
|
||||
props: PropTypes.object
|
||||
};
|
||||
|
||||
RCActivityIndicator.defaultProps = {
|
||||
theme: 'light'
|
||||
};
|
||||
|
||||
export default RCActivityIndicator;
|
||||
|
|
|
@ -2,13 +2,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Touch from '../utils/touch';
|
||||
|
||||
const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => (
|
||||
`${ baseUrl }${ url }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`
|
||||
);
|
||||
|
||||
const Avatar = React.memo(({
|
||||
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token
|
||||
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme
|
||||
}) => {
|
||||
const avatarStyle = {
|
||||
width: size,
|
||||
|
@ -39,7 +40,7 @@ const Avatar = React.memo(({
|
|||
}
|
||||
|
||||
|
||||
const image = (
|
||||
let image = (
|
||||
<FastImage
|
||||
style={avatarStyle}
|
||||
source={{
|
||||
|
@ -49,6 +50,14 @@ const Avatar = React.memo(({
|
|||
/>
|
||||
);
|
||||
|
||||
if (onPress) {
|
||||
image = (
|
||||
<Touch onPress={onPress} theme={theme}>
|
||||
{image}
|
||||
</Touch>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[avatarStyle, style]}>
|
||||
{image}
|
||||
|
@ -67,7 +76,9 @@ Avatar.propTypes = {
|
|||
type: PropTypes.string,
|
||||
children: PropTypes.object,
|
||||
userId: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
token: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
Avatar.defaultProps = {
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StyleSheet, Text, ActivityIndicator } from 'react-native';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
|
||||
import { COLOR_BUTTON_PRIMARY } from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const colors = {
|
||||
background_primary: COLOR_BUTTON_PRIMARY,
|
||||
background_secondary: 'white',
|
||||
|
||||
text_color_primary: 'white',
|
||||
text_color_secondary: COLOR_BUTTON_PRIMARY
|
||||
};
|
||||
import ActivityIndicator from '../ActivityIndicator';
|
||||
|
||||
/* eslint-disable react-native/no-unused-styles */
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -26,23 +19,6 @@ const styles = StyleSheet.create({
|
|||
text: {
|
||||
fontSize: 18,
|
||||
textAlign: 'center'
|
||||
},
|
||||
background_primary: {
|
||||
backgroundColor: colors.background_primary
|
||||
},
|
||||
background_secondary: {
|
||||
backgroundColor: colors.background_secondary
|
||||
},
|
||||
text_primary: {
|
||||
...sharedStyles.textMedium,
|
||||
color: colors.text_color_primary
|
||||
},
|
||||
text_secondary: {
|
||||
...sharedStyles.textBold,
|
||||
color: colors.text_color_secondary
|
||||
},
|
||||
disabled: {
|
||||
backgroundColor: '#e1e5e8'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -54,6 +30,7 @@ export default class Button extends React.PureComponent {
|
|||
disabled: PropTypes.bool,
|
||||
backgroundColor: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
style: PropTypes.any
|
||||
}
|
||||
|
||||
|
@ -67,24 +44,37 @@ export default class Button extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const {
|
||||
title, type, onPress, disabled, backgroundColor, loading, style, ...otherProps
|
||||
title, type, onPress, disabled, backgroundColor, loading, style, theme, ...otherProps
|
||||
} = this.props;
|
||||
const isPrimary = type === 'primary';
|
||||
return (
|
||||
<RectButton
|
||||
onPress={onPress}
|
||||
enabled={!(disabled || loading)}
|
||||
style={[
|
||||
styles.container,
|
||||
backgroundColor ? { backgroundColor } : styles[`background_${ type }`],
|
||||
disabled && styles.disabled,
|
||||
backgroundColor
|
||||
? { backgroundColor }
|
||||
: { backgroundColor: isPrimary ? themes[theme].actionTintColor : themes[theme].backgroundColor },
|
||||
disabled && { backgroundColor: themes[theme].borderColor },
|
||||
style
|
||||
]}
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
loading
|
||||
? <ActivityIndicator color={colors[`text_color_${ type }`]} />
|
||||
: <Text style={[styles.text, styles[`text_${ type }`]]}>{title}</Text>
|
||||
? <ActivityIndicator color={isPrimary ? themes[theme].buttonText : themes[theme].actionTintColor} />
|
||||
: (
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
isPrimary ? sharedStyles.textMedium : sharedStyles.textBold,
|
||||
{ color: isPrimary ? themes[theme].buttonText : themes[theme].actionTintColor }
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
</RectButton>
|
||||
);
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
marginHorizontal: 15,
|
||||
...sharedStyles.textColorDescription
|
||||
marginHorizontal: 15
|
||||
}
|
||||
});
|
||||
|
||||
const Check = React.memo(() => <CustomIcon style={styles.icon} size={22} name='check' />);
|
||||
const Check = React.memo(({ theme }) => <CustomIcon style={styles.icon} color={themes[theme].tintColor} size={22} name='check' />);
|
||||
|
||||
Check.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Check;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import React from 'react';
|
||||
import { View, Image, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
disclosureContainer: {
|
||||
|
@ -14,12 +17,23 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export const DisclosureImage = React.memo(() => <Image source={{ uri: 'disclosure_indicator' }} style={styles.disclosureIndicator} />);
|
||||
export const DisclosureImage = React.memo(({ theme }) => (
|
||||
<Image
|
||||
source={{ uri: 'disclosure_indicator' }}
|
||||
style={[styles.disclosureIndicator, { tintColor: themes[theme].auxiliaryTintColor }]}
|
||||
/>
|
||||
));
|
||||
DisclosureImage.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
const DisclosureIndicator = React.memo(() => (
|
||||
const DisclosureIndicator = React.memo(({ theme }) => (
|
||||
<View style={styles.disclosureContainer}>
|
||||
<DisclosureImage />
|
||||
<DisclosureImage theme={theme} />
|
||||
</View>
|
||||
));
|
||||
DisclosureIndicator.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default DisclosureIndicator;
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import { Text, TouchableOpacity, FlatList } from 'react-native';
|
||||
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
import { OptimizedFlatList } from 'react-native-optimized-flatlist';
|
||||
|
||||
import styles from './styles';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
|
||||
const EMOJIS_PER_ROW = isIOS ? 8 : 9;
|
||||
const EMOJI_SIZE = 50;
|
||||
|
||||
const renderEmoji = (emoji, size, baseUrl) => {
|
||||
if (emoji.isCustom) {
|
||||
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} baseUrl={baseUrl} />;
|
||||
if (emoji && emoji.isCustom) {
|
||||
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} emoji={emoji} baseUrl={baseUrl} />;
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
||||
|
@ -33,44 +31,41 @@ class EmojiCategory extends React.Component {
|
|||
width: PropTypes.number
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { window, width, emojisPerRow } = this.props;
|
||||
const { width: widthWidth, height: windowHeight } = window;
|
||||
|
||||
this.size = Math.min(width || widthWidth, windowHeight) / (emojisPerRow || EMOJIS_PER_ROW);
|
||||
this.emojis = props.emojis;
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
renderItem(emoji, size) {
|
||||
renderItem(emoji) {
|
||||
const { baseUrl, onEmojiSelected } = this.props;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
key={emoji.isCustom ? emoji.content : emoji}
|
||||
key={emoji && emoji.isCustom ? emoji.content : emoji}
|
||||
onPress={() => onEmojiSelected(emoji)}
|
||||
testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`}
|
||||
testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`}
|
||||
>
|
||||
{renderEmoji(emoji, size, baseUrl)}
|
||||
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { emojis } = this.props;
|
||||
const { emojis, width } = this.props;
|
||||
|
||||
if (!width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numColumns = Math.trunc(width / EMOJI_SIZE);
|
||||
const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2;
|
||||
|
||||
return (
|
||||
<OptimizedFlatList
|
||||
keyExtractor={item => (item.isCustom && item.content) || item}
|
||||
<FlatList
|
||||
contentContainerStyle={{ marginHorizontal }}
|
||||
// rerender FlatList in case of width changes
|
||||
key={`emoji-category-${ width }`}
|
||||
keyExtractor={item => (item && item.isCustom && item.content) || item}
|
||||
data={emojis}
|
||||
renderItem={({ item }) => this.renderItem(item, this.size)}
|
||||
numColumns={EMOJIS_PER_ROW}
|
||||
extraData={this.props}
|
||||
renderItem={({ item }) => this.renderItem(item)}
|
||||
numColumns={numColumns}
|
||||
initialNumToRender={45}
|
||||
getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })}
|
||||
removeClippedSubviews
|
||||
{...scrollPersistTaps}
|
||||
/>
|
||||
|
|
|
@ -2,26 +2,31 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { View, TouchableOpacity, Text } from 'react-native';
|
||||
import styles from './styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
export default class TabBar extends React.Component {
|
||||
static propTypes = {
|
||||
goToPage: PropTypes.func,
|
||||
activeTab: PropTypes.number,
|
||||
tabs: PropTypes.array,
|
||||
tabEmojiStyle: PropTypes.object
|
||||
tabEmojiStyle: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { activeTab } = this.props;
|
||||
const { activeTab, theme } = this.props;
|
||||
if (nextProps.activeTab !== activeTab) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
tabs, goToPage, tabEmojiStyle, activeTab
|
||||
tabs, goToPage, tabEmojiStyle, activeTab, theme
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -35,7 +40,7 @@ export default class TabBar extends React.Component {
|
|||
testID={`reaction-picker-${ tab }`}
|
||||
>
|
||||
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
|
||||
{activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />}
|
||||
{activeTab === i ? <View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} /> : <View style={styles.tabLine} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ScrollView } from 'react-native';
|
||||
import ScrollableTabView from 'react-native-scrollable-tab-view';
|
||||
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||
import equal from 'deep-equal';
|
||||
|
@ -16,6 +16,8 @@ import database from '../../lib/database';
|
|||
import { emojisByCategory } from '../../emojis';
|
||||
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
|
||||
import log from '../../utils/log';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
|
||||
const scrollProps = {
|
||||
keyboardShouldPersistTaps: 'always',
|
||||
|
@ -28,8 +30,7 @@ class EmojiPicker extends Component {
|
|||
customEmojis: PropTypes.object,
|
||||
onEmojiSelected: PropTypes.func,
|
||||
tabEmojiStyle: PropTypes.object,
|
||||
emojisPerRow: PropTypes.number,
|
||||
width: PropTypes.number
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -44,7 +45,8 @@ class EmojiPicker extends Component {
|
|||
this.state = {
|
||||
frequentlyUsed: [],
|
||||
customEmojis,
|
||||
show: false
|
||||
show: false,
|
||||
width: null
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -54,12 +56,15 @@ class EmojiPicker extends Component {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { frequentlyUsed, show } = this.state;
|
||||
const { width } = this.props;
|
||||
const { frequentlyUsed, show, width } = this.state;
|
||||
const { theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.show !== show) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.width !== width) {
|
||||
if (nextState.width !== width) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextState.frequentlyUsed, frequentlyUsed)) {
|
||||
|
@ -91,22 +96,24 @@ class EmojiPicker extends Component {
|
|||
_addFrequentlyUsed = protectedFunction(async(emoji) => {
|
||||
const db = database.active;
|
||||
const freqEmojiCollection = db.collections.get('frequently_used_emojis');
|
||||
let freqEmojiRecord;
|
||||
try {
|
||||
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
await db.action(async() => {
|
||||
try {
|
||||
const freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
|
||||
if (freqEmojiRecord) {
|
||||
await freqEmojiRecord.update((f) => {
|
||||
f.count += 1;
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await freqEmojiCollection.create((f) => {
|
||||
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
|
||||
Object.assign(f, emoji);
|
||||
f.count = 1;
|
||||
});
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
} else {
|
||||
await freqEmojiCollection.create((f) => {
|
||||
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
|
||||
Object.assign(f, emoji);
|
||||
f.count = 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
|
@ -124,11 +131,11 @@ class EmojiPicker extends Component {
|
|||
this.setState({ frequentlyUsed });
|
||||
}
|
||||
|
||||
renderCategory(category, i) {
|
||||
const { frequentlyUsed, customEmojis } = this.state;
|
||||
const {
|
||||
emojisPerRow, width, baseUrl
|
||||
} = this.props;
|
||||
onLayout = ({ nativeEvent: { layout: { width } } }) => this.setState({ width });
|
||||
|
||||
renderCategory(category, i, label) {
|
||||
const { frequentlyUsed, customEmojis, width } = this.state;
|
||||
const { baseUrl } = this.props;
|
||||
|
||||
let emojis = [];
|
||||
if (i === 0) {
|
||||
|
@ -143,41 +150,36 @@ class EmojiPicker extends Component {
|
|||
emojis={emojis}
|
||||
onEmojiSelected={emoji => this.onEmojiSelected(emoji)}
|
||||
style={styles.categoryContainer}
|
||||
size={emojisPerRow}
|
||||
width={width}
|
||||
baseUrl={baseUrl}
|
||||
tabLabel={label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { show, frequentlyUsed } = this.state;
|
||||
const { tabEmojiStyle } = this.props;
|
||||
const { tabEmojiStyle, theme } = this.props;
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ScrollableTabView
|
||||
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} />}
|
||||
contentProps={scrollProps}
|
||||
style={styles.background}
|
||||
>
|
||||
{
|
||||
categories.tabs.map((tab, i) => (
|
||||
(i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab
|
||||
: (
|
||||
<ScrollView
|
||||
key={tab.category}
|
||||
tabLabel={tab.tabLabel}
|
||||
style={styles.background}
|
||||
{...scrollProps}
|
||||
>
|
||||
{this.renderCategory(tab.category, i)}
|
||||
</ScrollView>
|
||||
)))
|
||||
}
|
||||
</ScrollableTabView>
|
||||
<View onLayout={this.onLayout} style={{ flex: 1 }}>
|
||||
<ScrollableTabView
|
||||
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
|
||||
contentProps={scrollProps}
|
||||
style={{ backgroundColor: themes[theme].focusedBackground }}
|
||||
>
|
||||
{
|
||||
categories.tabs.map((tab, i) => (
|
||||
(i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab
|
||||
: (
|
||||
this.renderCategory(tab.category, i, tab.tabLabel)
|
||||
)))
|
||||
}
|
||||
</ScrollableTabView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -186,4 +188,4 @@ const mapStateToProps = state => ({
|
|||
customEmojis: state.customEmojis
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(EmojiPicker);
|
||||
export default connect(mapStateToProps)(withTheme(EmojiPicker));
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
export default StyleSheet.create({
|
||||
background: {
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
|
@ -28,7 +24,6 @@ export default StyleSheet.create({
|
|||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
backgroundColor: COLOR_PRIMARY,
|
||||
bottom: 0
|
||||
},
|
||||
tabLine: {
|
||||
|
@ -51,11 +46,10 @@ export default StyleSheet.create({
|
|||
flex: 1
|
||||
},
|
||||
categoryEmoji: {
|
||||
color: 'black',
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'center'
|
||||
},
|
||||
customCategoryEmoji: {
|
||||
margin: 4
|
||||
margin: 8
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
|
||||
View, Text, TouchableWithoutFeedback, StyleSheet, SafeAreaView
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -9,8 +9,10 @@ import ImageViewer from 'react-native-image-zoom-viewer';
|
|||
import { Video } from 'expo-av';
|
||||
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_WHITE } from '../constants/colors';
|
||||
import { formatAttachmentUrl } from '../lib/utils';
|
||||
import ActivityIndicator from './ActivityIndicator';
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
|
@ -25,40 +27,22 @@ const styles = StyleSheet.create({
|
|||
marginVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
description: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
indicator: {
|
||||
flex: 1
|
||||
},
|
||||
video: {
|
||||
flex: 1
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
const Indicator = React.memo(() => (
|
||||
<ActivityIndicator style={styles.indicator} />
|
||||
));
|
||||
|
||||
const ModalContent = React.memo(({
|
||||
attachment, onClose, user, baseUrl
|
||||
attachment, onClose, user, baseUrl, theme
|
||||
}) => {
|
||||
if (attachment && attachment.image_url) {
|
||||
const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
|
||||
|
@ -66,8 +50,8 @@ const ModalContent = React.memo(({
|
|||
<SafeAreaView style={styles.safeArea}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{attachment.title}</Text>
|
||||
{attachment.description ? <Text style={styles.description}>{attachment.description}</Text> : null}
|
||||
<Text style={[styles.title, { color: themes[theme].buttonText }]}>{attachment.title}</Text>
|
||||
{attachment.description ? <Text style={[styles.description, { color: themes[theme].buttonText }]}>{attachment.description}</Text> : null}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<ImageViewer
|
||||
|
@ -78,7 +62,7 @@ const ModalContent = React.memo(({
|
|||
onSwipeDown={onClose}
|
||||
renderIndicator={() => null}
|
||||
renderImage={props => <FastImage {...props} />}
|
||||
loadingRender={() => <Indicator />}
|
||||
loadingRender={() => <ActivityIndicator size='large' theme={theme} />}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
@ -102,7 +86,7 @@ const ModalContent = React.memo(({
|
|||
onLoadStart={() => setLoading(true)}
|
||||
onError={console.log}
|
||||
/>
|
||||
{ loading ? <ActivityIndicator size='large' style={styles.loading} /> : null }
|
||||
{ loading ? <ActivityIndicator size='large' theme={theme} absolute /> : null }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -110,7 +94,7 @@ const ModalContent = React.memo(({
|
|||
});
|
||||
|
||||
const FileModal = React.memo(({
|
||||
isVisible, onClose, attachment, user, baseUrl
|
||||
isVisible, onClose, attachment, user, baseUrl, theme
|
||||
}) => (
|
||||
<Modal
|
||||
style={styles.modal}
|
||||
|
@ -120,15 +104,18 @@ const FileModal = React.memo(({
|
|||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'down']}
|
||||
>
|
||||
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
|
||||
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} theme={theme} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading);
|
||||
), (prevProps, nextProps) => (
|
||||
prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading && prevProps.theme === nextProps.theme
|
||||
));
|
||||
|
||||
FileModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
attachment: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
FileModal.displayName = 'FileModal';
|
||||
|
@ -137,8 +124,9 @@ ModalContent.propTypes = {
|
|||
attachment: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ModalContent.displayName = 'FileModalContent';
|
||||
|
||||
export default FileModal;
|
||||
export default withTheme(FileModal);
|
||||
|
|
|
@ -3,16 +3,25 @@ import PropTypes from 'prop-types';
|
|||
import { HeaderButtons, HeaderButton, Item } from 'react-navigation-header-buttons';
|
||||
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors';
|
||||
import { isIOS, isAndroid } from '../utils/deviceInfo';
|
||||
import { themes } from '../constants/colors';
|
||||
import I18n from '../i18n';
|
||||
import { withTheme } from '../theme';
|
||||
|
||||
const color = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
|
||||
export const headerIconSize = 23;
|
||||
|
||||
const CustomHeaderButton = React.memo(props => (
|
||||
<HeaderButton {...props} IconComponent={CustomIcon} iconSize={headerIconSize} color={color} />
|
||||
));
|
||||
const CustomHeaderButton = React.memo(withTheme(({ theme, ...props }) => (
|
||||
<HeaderButton
|
||||
{...props}
|
||||
IconComponent={CustomIcon}
|
||||
iconSize={headerIconSize}
|
||||
color={
|
||||
isAndroid
|
||||
? themes[theme].headerTitleColor
|
||||
: themes[theme].headerTintColor
|
||||
}
|
||||
/>
|
||||
)));
|
||||
|
||||
export const CustomHeaderButtons = React.memo(props => (
|
||||
<HeaderButtons
|
||||
|
@ -52,6 +61,9 @@ export const LegalButton = React.memo(({ navigation, testID }) => (
|
|||
<MoreButton onPress={() => navigation.navigate('LegalView')} testID={testID} />
|
||||
));
|
||||
|
||||
CustomHeaderButton.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
DrawerButton.propTypes = {
|
||||
navigation: PropTypes.object.isRequired,
|
||||
testID: PropTypes.string.isRequired
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
|
||||
import { COLOR_TEXT } from '../constants/colors';
|
||||
import Touch from '../utils/touch';
|
||||
import { themes } from '../constants/colors';
|
||||
import sharedStyles from '../views/Styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -12,7 +12,7 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 56,
|
||||
height: 46,
|
||||
paddingHorizontal: 15
|
||||
},
|
||||
disabled: {
|
||||
|
@ -24,24 +24,22 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
||||
const Content = React.memo(({
|
||||
title, subtitle, disabled, testID, right
|
||||
title, subtitle, disabled, testID, right, color, theme
|
||||
}) => (
|
||||
<View style={[styles.container, disabled && styles.disabled]} testID={testID}>
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={[styles.title, { color: color || themes[theme].titleText }]}>{title}</Text>
|
||||
{subtitle
|
||||
? <Text style={styles.subtitle}>{subtitle}</Text>
|
||||
? <Text style={[styles.subtitle, { color: themes[theme].bodyText }]}>{subtitle}</Text>
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
|
@ -52,25 +50,30 @@ const Content = React.memo(({
|
|||
const Button = React.memo(({
|
||||
onPress, ...props
|
||||
}) => (
|
||||
<RectButton
|
||||
<Touch
|
||||
onPress={onPress}
|
||||
activeOpacity={0.1}
|
||||
underlayColor={COLOR_TEXT}
|
||||
style={{ backgroundColor: themes[props.theme].backgroundColor }}
|
||||
enabled={!props.disabled}
|
||||
theme={props.theme}
|
||||
>
|
||||
<Content {...props} />
|
||||
</RectButton>
|
||||
</Touch>
|
||||
));
|
||||
|
||||
const Item = React.memo(({ ...props }) => {
|
||||
if (props.onPress) {
|
||||
return <Button {...props} />;
|
||||
}
|
||||
return <Content {...props} />;
|
||||
return (
|
||||
<View style={{ backgroundColor: themes[props.theme].backgroundColor }}>
|
||||
<Content {...props} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
Item.propTypes = {
|
||||
onPress: PropTypes.func
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
Content.propTypes = {
|
||||
|
@ -78,12 +81,15 @@ Content.propTypes = {
|
|||
subtitle: PropTypes.string,
|
||||
right: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
testID: PropTypes.string
|
||||
testID: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
color: PropTypes.string
|
||||
};
|
||||
|
||||
Button.propTypes = {
|
||||
onPress: PropTypes.func,
|
||||
disabled: PropTypes.bool
|
||||
disabled: PropTypes.bool,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import styles from './styles';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_PRIMARY } from '../../constants/colors';
|
||||
|
||||
export default class CommandPreview extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onPress: PropTypes.func,
|
||||
item: PropTypes.object
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { loading: true };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onPress, item } = this.props;
|
||||
const { loading } = this.state;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.commandPreview}
|
||||
onPress={() => onPress(item)}
|
||||
testID={`command-preview-item${ item.id }`}
|
||||
>
|
||||
{item.type === 'image'
|
||||
? (
|
||||
<FastImage
|
||||
style={styles.commandPreviewImage}
|
||||
source={{ uri: item.value }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
onLoadStart={() => this.setState({ loading: true })}
|
||||
onLoad={() => this.setState({ loading: false })}
|
||||
>
|
||||
{ loading ? <ActivityIndicator /> : null }
|
||||
</FastImage>
|
||||
)
|
||||
: <CustomIcon name='file-generic' size={36} color={COLOR_PRIMARY} />
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import styles from '../styles';
|
||||
import { CustomIcon } from '../../../lib/Icons';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import MessageboxContext from '../Context';
|
||||
import ActivityIndicator from '../../ActivityIndicator';
|
||||
|
||||
const Item = ({ item, theme }) => {
|
||||
const context = useContext(MessageboxContext);
|
||||
const { onPressCommandPreview } = context;
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.commandPreview}
|
||||
onPress={() => onPressCommandPreview(item)}
|
||||
testID={`command-preview-item${ item.id }`}
|
||||
>
|
||||
{item.type === 'image'
|
||||
? (
|
||||
<FastImage
|
||||
style={styles.commandPreviewImage}
|
||||
source={{ uri: item.value }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
onLoadStart={() => setLoading(true)}
|
||||
onLoad={() => setLoading(false)}
|
||||
>
|
||||
{ loading ? <ActivityIndicator theme={theme} /> : null }
|
||||
</FastImage>
|
||||
)
|
||||
: <CustomIcon name='file-generic' size={36} color={themes[theme].actionTintColor} />
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
Item.propTypes = {
|
||||
item: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Item;
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import equal from 'deep-equal';
|
||||
|
||||
import Item from './Item';
|
||||
import styles from '../styles';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { withTheme } from '../../../theme';
|
||||
|
||||
const CommandsPreview = React.memo(({ theme, commandPreview, showCommandPreview }) => {
|
||||
if (!showCommandPreview) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
testID='commandbox-container'
|
||||
style={[styles.mentionList, { backgroundColor: themes[theme].messageboxBackground }]}
|
||||
data={commandPreview}
|
||||
renderItem={({ item }) => <Item item={item} theme={theme} />}
|
||||
keyExtractor={item => item.id}
|
||||
keyboardShouldPersistTaps='always'
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.commandPreview, nextProps.commandPreview)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
CommandsPreview.propTypes = {
|
||||
commandPreview: PropTypes.array,
|
||||
showCommandPreview: PropTypes.bool,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default withTheme(CommandsPreview);
|
|
@ -0,0 +1,4 @@
|
|||
import React from 'react';
|
||||
|
||||
const MessageboxContext = React.createContext();
|
||||
export default MessageboxContext;
|
|
@ -1,12 +1,19 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { KeyboardRegistry } from 'react-native-keyboard-input';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import store from '../../lib/createStore';
|
||||
import EmojiPicker from '../EmojiPicker';
|
||||
import styles from './styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
|
||||
export default class EmojiKeyboard extends React.PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const state = store.getState();
|
||||
|
@ -18,11 +25,12 @@ export default class EmojiKeyboard extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'>
|
||||
<EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} baseUrl={this.baseUrl} />
|
||||
<View style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} testID='messagebox-keyboard-emoji'>
|
||||
<EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
KeyboardRegistry.registerKeyboard('EmojiKeyboard', () => EmojiKeyboard);
|
||||
KeyboardRegistry.registerKeyboard('EmojiKeyboard', () => withTheme(EmojiKeyboard));
|
||||
|
|
|
@ -4,21 +4,23 @@ import PropTypes from 'prop-types';
|
|||
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
|
||||
|
||||
const LeftButtons = React.memo(({
|
||||
showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji
|
||||
theme, showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji
|
||||
}) => {
|
||||
if (editing) {
|
||||
return <CancelEditingButton onPress={editCancel} />;
|
||||
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
||||
}
|
||||
return (
|
||||
<ToggleEmojiButton
|
||||
show={showEmojiKeyboard}
|
||||
open={openEmoji}
|
||||
close={closeEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
LeftButtons.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
showEmojiKeyboard: PropTypes.bool,
|
||||
openEmoji: PropTypes.func.isRequired,
|
||||
closeEmoji: PropTypes.func.isRequired,
|
||||
|
|
|
@ -4,15 +4,16 @@ import PropTypes from 'prop-types';
|
|||
import { CancelEditingButton, FileButton } from './buttons';
|
||||
|
||||
const LeftButtons = React.memo(({
|
||||
showFileActions, editing, editCancel
|
||||
theme, showFileActions, editing, editCancel
|
||||
}) => {
|
||||
if (editing) {
|
||||
return <CancelEditingButton onPress={editCancel} />;
|
||||
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
||||
}
|
||||
return <FileButton onPress={showFileActions} />;
|
||||
return <FileButton onPress={showFileActions} theme={theme} />;
|
||||
});
|
||||
|
||||
LeftButtons.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
showFileActions: PropTypes.func.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
editCancel: PropTypes.func.isRequired
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { TouchableOpacity, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from '../styles';
|
||||
import I18n from '../../../i18n';
|
||||
import { themes } from '../../../constants/colors';
|
||||
|
||||
const FixedMentionItem = ({ item, onPress, theme }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.mentionItem,
|
||||
{
|
||||
backgroundColor: themes[theme].auxiliaryBackground,
|
||||
borderTopColor: themes[theme].separatorColor
|
||||
}
|
||||
]}
|
||||
onPress={() => onPress(item)}
|
||||
>
|
||||
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
|
||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>
|
||||
{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
FixedMentionItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default FixedMentionItem;
|
|
@ -0,0 +1,34 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||
|
||||
import styles from '../styles';
|
||||
import MessageboxContext from '../Context';
|
||||
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
|
||||
|
||||
const MentionEmoji = ({ item }) => {
|
||||
const context = useContext(MessageboxContext);
|
||||
const { baseUrl } = context;
|
||||
|
||||
if (item.name) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
style={styles.mentionItemCustomEmoji}
|
||||
emoji={item}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text style={styles.mentionItemEmoji}>
|
||||
{shortnameToUnicode(`:${ item }:`)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
MentionEmoji.propTypes = {
|
||||
item: PropTypes.object
|
||||
};
|
||||
|
||||
export default MentionEmoji;
|
|
@ -0,0 +1,95 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { TouchableOpacity, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from '../styles';
|
||||
import Avatar from '../../Avatar';
|
||||
import MessageboxContext from '../Context';
|
||||
import FixedMentionItem from './FixedMentionItem';
|
||||
import MentionEmoji from './MentionEmoji';
|
||||
import {
|
||||
MENTIONS_TRACKING_TYPE_EMOJIS,
|
||||
MENTIONS_TRACKING_TYPE_COMMANDS
|
||||
} from '../constants';
|
||||
import { themes } from '../../../constants/colors';
|
||||
|
||||
const MentionItem = ({
|
||||
item, trackingType, theme
|
||||
}) => {
|
||||
const context = useContext(MessageboxContext);
|
||||
const { baseUrl, user, onPressMention } = context;
|
||||
|
||||
const defineTestID = (type) => {
|
||||
switch (type) {
|
||||
case MENTIONS_TRACKING_TYPE_EMOJIS:
|
||||
return `mention-item-${ item.name || item }`;
|
||||
case MENTIONS_TRACKING_TYPE_COMMANDS:
|
||||
return `mention-item-${ item.command || item }`;
|
||||
default:
|
||||
return `mention-item-${ item.username || item.name || item }`;
|
||||
}
|
||||
};
|
||||
|
||||
const testID = defineTestID(trackingType);
|
||||
|
||||
if (item.username === 'all' || item.username === 'here') {
|
||||
return <FixedMentionItem item={item} onPress={onPressMention} theme={theme} />;
|
||||
}
|
||||
|
||||
let content = (
|
||||
<>
|
||||
<Avatar
|
||||
style={styles.avatar}
|
||||
text={item.username || item.name}
|
||||
size={30}
|
||||
type={item.t}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
if (trackingType === MENTIONS_TRACKING_TYPE_EMOJIS) {
|
||||
content = (
|
||||
<>
|
||||
<MentionEmoji item={item} />
|
||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{ item.name || item }:</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) {
|
||||
content = (
|
||||
<>
|
||||
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
|
||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.command}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.mentionItem,
|
||||
{
|
||||
backgroundColor: themes[theme].auxiliaryBackground,
|
||||
borderTopColor: themes[theme].separatorColor
|
||||
}
|
||||
]}
|
||||
onPress={() => onPressMention(item)}
|
||||
testID={testID}
|
||||
>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
MentionItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
trackingType: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default MentionItem;
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import equal from 'deep-equal';
|
||||
|
||||
import styles from '../styles';
|
||||
import MentionItem from './MentionItem';
|
||||
import { themes } from '../../../constants/colors';
|
||||
|
||||
const Mentions = React.memo(({ mentions, trackingType, theme }) => {
|
||||
if (!trackingType) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
testID='messagebox-container'
|
||||
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||
data={mentions}
|
||||
extraData={mentions}
|
||||
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
|
||||
keyExtractor={item => item.id || item.username || item.command || item}
|
||||
keyboardShouldPersistTaps='always'
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.trackingType !== nextProps.trackingType) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.mentions, nextProps.mentions)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Mentions.propTypes = {
|
||||
mentions: PropTypes.array,
|
||||
trackingType: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Mentions;
|
|
@ -11,7 +11,7 @@ import styles from './styles';
|
|||
import I18n from '../../i18n';
|
||||
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_SUCCESS, COLOR_DANGER } from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
export const _formatTime = function(seconds) {
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
|
@ -37,6 +37,7 @@ export default class extends React.PureComponent {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
theme: PropTypes.string,
|
||||
onFinish: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
|
@ -122,14 +123,17 @@ export default class extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { currentTime } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
key='messagebox-recording'
|
||||
testID='messagebox-recording'
|
||||
style={styles.textBox}
|
||||
style={[
|
||||
styles.textBox,
|
||||
{ borderTopColor: themes[theme].borderColor }
|
||||
]}
|
||||
>
|
||||
<View style={styles.textArea}>
|
||||
<View style={[styles.textArea, { backgroundColor: themes[theme].messageboxBackground }]}>
|
||||
<BorderlessButton
|
||||
onPress={this.cancelAudioMessage}
|
||||
accessibilityLabel={I18n.t('Cancel_recording')}
|
||||
|
@ -138,11 +142,11 @@ export default class extends React.PureComponent {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={COLOR_DANGER}
|
||||
color={themes[theme].dangerColor}
|
||||
name='cross'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
<Text key='currentTime' style={styles.textBoxInput}>{currentTime}</Text>
|
||||
<Text key='currentTime' style={[styles.textBoxInput, { color: themes[theme].titleText }]}>{currentTime}</Text>
|
||||
<BorderlessButton
|
||||
onPress={this.finishAudioMessage}
|
||||
accessibilityLabel={I18n.t('Finish_recording')}
|
||||
|
@ -151,7 +155,7 @@ export default class extends React.PureComponent {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={COLOR_SUCCESS}
|
||||
color={themes[theme].successColor}
|
||||
name='check'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
|
@ -7,20 +7,16 @@ import { connect } from 'react-redux';
|
|||
import Markdown from '../markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_TEXT_DESCRIPTION, COLOR_WHITE
|
||||
} from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
backgroundColor: COLOR_WHITE
|
||||
paddingTop: 10
|
||||
},
|
||||
messageContainer: {
|
||||
flex: 1,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 4
|
||||
|
@ -30,7 +26,6 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center'
|
||||
},
|
||||
username: {
|
||||
color: COLOR_PRIMARY,
|
||||
fontSize: 16,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
|
@ -38,7 +33,6 @@ const styles = StyleSheet.create({
|
|||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
marginLeft: 6,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
},
|
||||
|
@ -47,45 +41,53 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
class ReplyPreview extends Component {
|
||||
static propTypes = {
|
||||
useMarkdown: PropTypes.bool,
|
||||
message: PropTypes.object.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
getCustomEmoji: PropTypes.func
|
||||
const ReplyPreview = React.memo(({
|
||||
message, Message_TimeFormat, baseUrl, username, useMarkdown, replying, getCustomEmoji, close, theme
|
||||
}) => {
|
||||
if (!replying) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
close = () => {
|
||||
const { close } = this.props;
|
||||
close();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
message, Message_TimeFormat, baseUrl, username, useMarkdown, getCustomEmoji
|
||||
} = this.props;
|
||||
const time = moment(message.ts).format(Message_TimeFormat);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.messageContainer}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.username}>{message.u.username}</Text>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} preview />
|
||||
const time = moment(message.ts).format(Message_TimeFormat);
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: themes[theme].messageboxBackground }
|
||||
]}
|
||||
>
|
||||
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u.username}</Text>
|
||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
</View>
|
||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||
<Markdown
|
||||
msg={message.msg}
|
||||
baseUrl={baseUrl}
|
||||
username={username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
numberOfLines={1}
|
||||
useMarkdown={useMarkdown}
|
||||
preview
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
<CustomIcon name='cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme);
|
||||
|
||||
ReplyPreview.propTypes = {
|
||||
replying: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
message: PropTypes.object.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
useMarkdown: state.markdown.useMarkdown,
|
||||
|
|
|
@ -4,20 +4,21 @@ import PropTypes from 'prop-types';
|
|||
import { SendButton, AudioButton, FileButton } from './buttons';
|
||||
|
||||
const RightButtons = React.memo(({
|
||||
showSend, submit, recordAudioMessage, showFileActions
|
||||
theme, showSend, submit, recordAudioMessage, showFileActions
|
||||
}) => {
|
||||
if (showSend) {
|
||||
return <SendButton onPress={submit} />;
|
||||
return <SendButton onPress={submit} theme={theme} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<AudioButton onPress={recordAudioMessage} />
|
||||
<FileButton onPress={showFileActions} />
|
||||
<AudioButton onPress={recordAudioMessage} theme={theme} />
|
||||
<FileButton onPress={showFileActions} theme={theme} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
RightButtons.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
showSend: PropTypes.bool,
|
||||
submit: PropTypes.func.isRequired,
|
||||
recordAudioMessage: PropTypes.func.isRequired,
|
||||
|
|
|
@ -4,15 +4,16 @@ import PropTypes from 'prop-types';
|
|||
import { SendButton, AudioButton } from './buttons';
|
||||
|
||||
const RightButtons = React.memo(({
|
||||
showSend, submit, recordAudioMessage
|
||||
theme, showSend, submit, recordAudioMessage
|
||||
}) => {
|
||||
if (showSend) {
|
||||
return <SendButton onPress={submit} />;
|
||||
return <SendButton theme={theme} onPress={submit} />;
|
||||
}
|
||||
return <AudioButton onPress={recordAudioMessage} />;
|
||||
return <AudioButton theme={theme} onPress={recordAudioMessage} />;
|
||||
});
|
||||
|
||||
RightButtons.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
showSend: PropTypes.bool,
|
||||
submit: PropTypes.func.isRequired,
|
||||
recordAudioMessage: PropTypes.func.isRequired
|
||||
|
|
|
@ -12,16 +12,16 @@ import Button from '../Button';
|
|||
import I18n from '../../i18n';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import {
|
||||
COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE
|
||||
} from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
|
||||
const cancelButtonColor = COLOR_BACKGROUND_CONTAINER;
|
||||
import { withTheme } from '../../theme';
|
||||
import { withSplit } from '../../split';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modal: {
|
||||
alignItems: 'center'
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
margin: 0
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
|
@ -30,12 +30,10 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorTitle,
|
||||
...sharedStyles.textBold
|
||||
},
|
||||
container: {
|
||||
height: 430,
|
||||
backgroundColor: COLOR_WHITE,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
scrollView: {
|
||||
|
@ -48,11 +46,13 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 16,
|
||||
resizeMode: 'contain'
|
||||
},
|
||||
bigPreview: {
|
||||
height: 250
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER
|
||||
padding: 16
|
||||
},
|
||||
button: {
|
||||
marginBottom: 0
|
||||
|
@ -68,7 +68,6 @@ const styles = StyleSheet.create({
|
|||
textAlign: 'center'
|
||||
},
|
||||
fileIcon: {
|
||||
color: COLOR_PRIMARY,
|
||||
margin: 20,
|
||||
flex: 1,
|
||||
textAlign: 'center'
|
||||
|
@ -77,7 +76,6 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
borderRadius: 4,
|
||||
height: 150,
|
||||
backgroundColor: '#1f2329',
|
||||
marginBottom: 6,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
|
@ -91,7 +89,9 @@ class UploadModal extends Component {
|
|||
file: PropTypes.object,
|
||||
close: PropTypes.func,
|
||||
submit: PropTypes.func,
|
||||
window: PropTypes.object
|
||||
window: PropTypes.object,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
}
|
||||
|
||||
state = {
|
||||
|
@ -113,11 +113,19 @@ class UploadModal extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { name, description, file } = this.state;
|
||||
const { window, isVisible } = this.props;
|
||||
const {
|
||||
window, isVisible, split, theme
|
||||
} = this.props;
|
||||
|
||||
if (nextState.name !== name) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.split !== split) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.description !== description) {
|
||||
return true;
|
||||
}
|
||||
|
@ -140,67 +148,69 @@ class UploadModal extends Component {
|
|||
}
|
||||
|
||||
renderButtons = () => {
|
||||
const { close } = this.props;
|
||||
const { close, theme } = this.props;
|
||||
if (isIOS) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<View style={[styles.buttonContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
||||
<Button
|
||||
title={I18n.t('Cancel')}
|
||||
type='secondary'
|
||||
backgroundColor={cancelButtonColor}
|
||||
backgroundColor={themes[theme].chatComponentBackground}
|
||||
style={styles.button}
|
||||
onPress={close}
|
||||
theme={theme}
|
||||
/>
|
||||
<Button
|
||||
title={I18n.t('Send')}
|
||||
type='primary'
|
||||
style={styles.button}
|
||||
onPress={this.submit}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
// FIXME: RNGH don't work well on Android modals: https://github.com/kmagiera/react-native-gesture-handler/issues/139
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<View style={[styles.buttonContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
||||
<TouchableHighlight
|
||||
onPress={close}
|
||||
style={[styles.androidButton, { backgroundColor: cancelButtonColor }]}
|
||||
underlayColor={cancelButtonColor}
|
||||
style={[styles.androidButton, { backgroundColor: themes[theme].chatComponentBackground }]}
|
||||
underlayColor={themes[theme].chatComponentBackground}
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: COLOR_PRIMARY }]}>{I18n.t('Cancel')}</Text>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: themes[theme].tintColor }]}>{I18n.t('Cancel')}</Text>
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
onPress={this.submit}
|
||||
style={[styles.androidButton, { backgroundColor: COLOR_PRIMARY }]}
|
||||
underlayColor={COLOR_PRIMARY}
|
||||
style={[styles.androidButton, { backgroundColor: themes[theme].tintColor }]}
|
||||
underlayColor={themes[theme].tintColor}
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: COLOR_WHITE }]}>{I18n.t('Send')}</Text>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: themes[theme].buttonText }]}>{I18n.t('Send')}</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderPreview() {
|
||||
const { file } = this.props;
|
||||
const { file, split, theme } = this.props;
|
||||
if (file.mime && file.mime.match(/image/)) {
|
||||
return (<Image source={{ isStatic: true, uri: file.path }} style={styles.image} />);
|
||||
return (<Image source={{ isStatic: true, uri: file.path }} style={[styles.image, split && styles.bigPreview]} />);
|
||||
}
|
||||
if (file.mime && file.mime.match(/video/)) {
|
||||
return (
|
||||
<View style={styles.video}>
|
||||
<CustomIcon name='play' size={72} color={COLOR_WHITE} />
|
||||
<View style={[styles.video, { backgroundColor: themes[theme].bannerBackground }]}>
|
||||
<CustomIcon name='play' size={72} color={themes[theme].buttonText} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (<CustomIcon name='file-generic' size={72} style={styles.fileIcon} />);
|
||||
return (<CustomIcon name='file-generic' size={72} style={[styles.fileIcon, { color: themes[theme].tintColor }]} />);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
window: { width }, isVisible, close
|
||||
window: { width }, isVisible, close, split, theme
|
||||
} = this.props;
|
||||
const { name, description } = this.state;
|
||||
return (
|
||||
|
@ -215,9 +225,9 @@ class UploadModal extends Component {
|
|||
hideModalContentWhileAnimating
|
||||
avoidKeyboard
|
||||
>
|
||||
<View style={[styles.container, { width: width - 32 }]}>
|
||||
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && sharedStyles.modal]}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{I18n.t('Upload_file_question_mark')}</Text>
|
||||
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.scrollView}>
|
||||
|
@ -226,11 +236,13 @@ class UploadModal extends Component {
|
|||
placeholder={I18n.t('File_name')}
|
||||
value={name}
|
||||
onChangeText={value => this.setState({ name: value })}
|
||||
theme={theme}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={I18n.t('File_description')}
|
||||
value={description}
|
||||
onChangeText={value => this.setState({ description: value })}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
{this.renderButtons()}
|
||||
|
@ -240,4 +252,4 @@ class UploadModal extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default responsive(UploadModal);
|
||||
export default responsive(withTheme(withSplit(UploadModal)));
|
||||
|
|
|
@ -3,16 +3,18 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const AudioButton = React.memo(({ onPress }) => (
|
||||
const AudioButton = React.memo(({ theme, onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-send-audio'
|
||||
accessibilityLabel='Send_audio_message'
|
||||
icon='mic'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
||||
AudioButton.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ import React from 'react';
|
|||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COLOR_PRIMARY } from '../../../constants/colors';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { CustomIcon } from '../../../lib/Icons';
|
||||
import styles from '../styles';
|
||||
import I18n from '../../../i18n';
|
||||
|
||||
const BaseButton = React.memo(({
|
||||
onPress, testID, accessibilityLabel, icon
|
||||
onPress, testID, accessibilityLabel, icon, theme
|
||||
}) => (
|
||||
<BorderlessButton
|
||||
onPress={onPress}
|
||||
|
@ -17,11 +17,12 @@ const BaseButton = React.memo(({
|
|||
accessibilityLabel={I18n.t(accessibilityLabel)}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name={icon} size={23} color={COLOR_PRIMARY} />
|
||||
<CustomIcon name={icon} size={23} color={themes[theme].tintColor} />
|
||||
</BorderlessButton>
|
||||
));
|
||||
|
||||
BaseButton.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
testID: PropTypes.string.isRequired,
|
||||
accessibilityLabel: PropTypes.string.isRequired,
|
||||
|
|
|
@ -3,16 +3,18 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const CancelEditingButton = React.memo(({ onPress }) => (
|
||||
const CancelEditingButton = React.memo(({ theme, onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-cancel-editing'
|
||||
accessibilityLabel='Cancel_editing'
|
||||
icon='cross'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
||||
CancelEditingButton.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const FileButton = React.memo(({ onPress }) => (
|
||||
const FileButton = React.memo(({ theme, onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-actions'
|
||||
accessibilityLabel='Message_actions'
|
||||
icon='plus'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
||||
FileButton.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const SendButton = React.memo(({ onPress }) => (
|
||||
const SendButton = React.memo(({ theme, onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-send-message'
|
||||
accessibilityLabel='Send_message'
|
||||
icon='send1'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
||||
SendButton.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const ToggleEmojiButton = React.memo(({ show, open, close }) => {
|
||||
const ToggleEmojiButton = React.memo(({
|
||||
theme, show, open, close
|
||||
}) => {
|
||||
if (show) {
|
||||
return (
|
||||
<BaseButton
|
||||
|
@ -11,6 +13,7 @@ const ToggleEmojiButton = React.memo(({ show, open, close }) => {
|
|||
testID='messagebox-close-emoji'
|
||||
accessibilityLabel='Close_emoji_selector'
|
||||
icon='keyboard'
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -20,11 +23,13 @@ const ToggleEmojiButton = React.memo(({ show, open, close }) => {
|
|||
testID='messagebox-open-emoji'
|
||||
accessibilityLabel='Open_emoji_selector'
|
||||
icon='emoji'
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleEmojiButton.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
show: PropTypes.bool,
|
||||
open: PropTypes.func.isRequired,
|
||||
close: PropTypes.func.isRequired
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export const MENTIONS_TRACKING_TYPE_USERS = '@';
|
||||
export const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
|
||||
export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
|
||||
export const MENTIONS_COUNT_TO_DISPLAY = 4;
|
|
@ -1,10 +1,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, TextInput, FlatList, Text, TouchableOpacity, Alert, ScrollView
|
||||
} from 'react-native';
|
||||
import { View, Alert, Keyboard } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||
import ImagePicker from 'react-native-image-crop-picker';
|
||||
import equal from 'deep-equal';
|
||||
|
@ -12,12 +9,11 @@ import DocumentPicker from 'react-native-document-picker';
|
|||
import ActionSheet from 'react-native-action-sheet';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
|
||||
import TextInput from '../../presentation/TextInput';
|
||||
import { userTyping as userTypingAction } from '../../actions/room';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import styles from './styles';
|
||||
import database from '../../lib/database';
|
||||
import Avatar from '../Avatar';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import { emojis } from '../../emojis';
|
||||
import Recording from './Recording';
|
||||
import UploadModal from './UploadModal';
|
||||
|
@ -25,17 +21,28 @@ import log from '../../utils/log';
|
|||
import I18n from '../../i18n';
|
||||
import ReplyPreview from './ReplyPreview';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import LeftButtons from './LeftButtons';
|
||||
import RightButtons from './RightButtons';
|
||||
import { isAndroid } from '../../utils/deviceInfo';
|
||||
import CommandPreview from './CommandPreview';
|
||||
import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
||||
import { canUploadFile } from '../../utils/media';
|
||||
|
||||
const MENTIONS_TRACKING_TYPE_USERS = '@';
|
||||
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
|
||||
const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
|
||||
const MENTIONS_COUNT_TO_DISPLAY = 4;
|
||||
import EventEmiter from '../../utils/events';
|
||||
import {
|
||||
KEY_COMMAND,
|
||||
handleCommandTyping,
|
||||
handleCommandSubmit,
|
||||
handleCommandShowUpload
|
||||
} from '../../commands';
|
||||
import Mentions from './Mentions';
|
||||
import MessageboxContext from './Context';
|
||||
import {
|
||||
MENTIONS_TRACKING_TYPE_EMOJIS,
|
||||
MENTIONS_TRACKING_TYPE_COMMANDS,
|
||||
MENTIONS_COUNT_TO_DISPLAY,
|
||||
MENTIONS_TRACKING_TYPE_USERS
|
||||
} from './constants';
|
||||
import CommandsPreview from './CommandsPreview';
|
||||
import { withTheme } from '../../theme';
|
||||
|
||||
const imagePickerConfig = {
|
||||
cropping: true,
|
||||
|
@ -65,7 +72,7 @@ class MessageBox extends Component {
|
|||
replying: PropTypes.bool,
|
||||
editing: PropTypes.bool,
|
||||
threadsEnabled: PropTypes.bool,
|
||||
isFocused: PropTypes.bool,
|
||||
isFocused: PropTypes.func,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
|
@ -81,6 +88,7 @@ class MessageBox extends Component {
|
|||
editRequest: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
typing: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
replyCancel: PropTypes.func
|
||||
}
|
||||
|
||||
|
@ -98,8 +106,8 @@ class MessageBox extends Component {
|
|||
commandPreview: [],
|
||||
showCommandPreview: false
|
||||
};
|
||||
this.onEmojiSelected = this.onEmojiSelected.bind(this);
|
||||
this.text = '';
|
||||
this.focused = false;
|
||||
this.fileOptions = [
|
||||
I18n.t('Cancel'),
|
||||
I18n.t('Take_a_photo'),
|
||||
|
@ -162,11 +170,15 @@ class MessageBox extends Component {
|
|||
if (isAndroid) {
|
||||
require('./EmojiKeyboard');
|
||||
}
|
||||
|
||||
if (isTablet) {
|
||||
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { isFocused, editing, replying } = this.props;
|
||||
if (!isFocused) {
|
||||
if (!isFocused()) {
|
||||
return;
|
||||
}
|
||||
if (editing !== nextProps.editing && nextProps.editing) {
|
||||
|
@ -187,9 +199,12 @@ class MessageBox extends Component {
|
|||
} = this.state;
|
||||
|
||||
const {
|
||||
roomType, replying, editing, isFocused
|
||||
roomType, replying, editing, isFocused, theme
|
||||
} = this.props;
|
||||
if (!isFocused) {
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
if (!isFocused()) {
|
||||
return false;
|
||||
}
|
||||
if (nextProps.roomType !== roomType) {
|
||||
|
@ -239,12 +254,16 @@ class MessageBox extends Component {
|
|||
if (this.getSlashCommands && this.getSlashCommands.stop) {
|
||||
this.getSlashCommands.stop();
|
||||
}
|
||||
if (isTablet) {
|
||||
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
|
||||
}
|
||||
}
|
||||
|
||||
onChangeText = (text) => {
|
||||
const isTextEmpty = text.length === 0;
|
||||
this.setShowSend(!isTextEmpty);
|
||||
this.debouncedOnChangeText(text);
|
||||
this.setInput(text);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
|
@ -253,7 +272,6 @@ class MessageBox extends Component {
|
|||
const isTextEmpty = text.length === 0;
|
||||
// this.setShowSend(!isTextEmpty);
|
||||
this.handleTyping(!isTextEmpty);
|
||||
this.setInput(text);
|
||||
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
||||
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
|
||||
if (slashCommand) {
|
||||
|
@ -453,7 +471,10 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
setShowSend = (showSend) => {
|
||||
this.setState({ showSend });
|
||||
const { showSend: prevShowSend } = this.state;
|
||||
if (prevShowSend !== showSend) {
|
||||
this.setState({ showSend });
|
||||
}
|
||||
}
|
||||
|
||||
clearInput = () => {
|
||||
|
@ -625,6 +646,7 @@ class MessageBox extends Component {
|
|||
const message = this.text;
|
||||
|
||||
this.clearInput();
|
||||
this.debouncedOnChangeText.stop();
|
||||
this.closeEmoji();
|
||||
this.stopTrackingMention();
|
||||
this.handleTyping(false);
|
||||
|
@ -726,180 +748,60 @@ class MessageBox extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
renderFixedMentionItem = item => (
|
||||
<TouchableOpacity
|
||||
style={styles.mentionItem}
|
||||
onPress={() => this.onPressMention(item)}
|
||||
>
|
||||
<Text style={styles.fixedMentionAvatar}>{item.username}</Text>
|
||||
<Text style={styles.mentionText}>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
||||
renderMentionEmoji = (item) => {
|
||||
const { baseUrl } = this.props;
|
||||
|
||||
if (item.name) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
key='mention-item-avatar'
|
||||
style={styles.mentionItemCustomEmoji}
|
||||
emoji={item}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
key='mention-item-avatar'
|
||||
style={styles.mentionItemEmoji}
|
||||
>
|
||||
{shortnameToUnicode(`:${ item }:`)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
renderMentionItem = ({ item }) => {
|
||||
const { trackingType } = this.state;
|
||||
const { baseUrl, user } = this.props;
|
||||
|
||||
if (item.username === 'all' || item.username === 'here') {
|
||||
return this.renderFixedMentionItem(item);
|
||||
}
|
||||
const defineTestID = (type) => {
|
||||
switch (type) {
|
||||
case MENTIONS_TRACKING_TYPE_EMOJIS:
|
||||
return `mention-item-${ item.name || item }`;
|
||||
case MENTIONS_TRACKING_TYPE_COMMANDS:
|
||||
return `mention-item-${ item.command || item }`;
|
||||
default:
|
||||
return `mention-item-${ item.username || item.name || item }`;
|
||||
handleCommands = ({ event }) => {
|
||||
if (handleCommandTyping(event)) {
|
||||
if (this.focused) {
|
||||
Keyboard.dismiss();
|
||||
} else {
|
||||
this.component.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const testID = defineTestID(trackingType);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.mentionItem}
|
||||
onPress={() => this.onPressMention(item)}
|
||||
testID={testID}
|
||||
>
|
||||
|
||||
{(() => {
|
||||
switch (trackingType) {
|
||||
case MENTIONS_TRACKING_TYPE_EMOJIS:
|
||||
return (
|
||||
<>
|
||||
{this.renderMentionEmoji(item)}
|
||||
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
|
||||
</>
|
||||
);
|
||||
case MENTIONS_TRACKING_TYPE_COMMANDS:
|
||||
return (
|
||||
<>
|
||||
<Text key='mention-item-command' style={styles.slash}>/</Text>
|
||||
<Text key='mention-item-param'>{ item.command}</Text>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Avatar
|
||||
key='mention-item-avatar'
|
||||
style={styles.avatar}
|
||||
text={item.username || item.name}
|
||||
size={30}
|
||||
type={item.t}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name || item }</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
this.focused = !this.focused;
|
||||
} else if (handleCommandSubmit(event)) {
|
||||
this.submit();
|
||||
} else if (handleCommandShowUpload(event)) {
|
||||
this.showFileActions();
|
||||
}
|
||||
}
|
||||
|
||||
renderMentions = () => {
|
||||
const { mentions, trackingType } = this.state;
|
||||
if (!trackingType) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ScrollView
|
||||
testID='messagebox-container'
|
||||
style={styles.scrollViewMention}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<FlatList
|
||||
style={styles.mentionList}
|
||||
data={mentions}
|
||||
extraData={mentions}
|
||||
renderItem={this.renderMentionItem}
|
||||
keyExtractor={item => item.id || item.username || item.command || item}
|
||||
keyboardShouldPersistTaps='always'
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
renderCommandPreviewItem = ({ item }) => (
|
||||
<CommandPreview item={item} onPress={this.onPressCommandPreview} />
|
||||
);
|
||||
|
||||
renderCommandPreview = () => {
|
||||
const { commandPreview, showCommandPreview } = this.state;
|
||||
if (!showCommandPreview) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View key='commandbox-container' testID='commandbox-container'>
|
||||
<FlatList
|
||||
style={styles.mentionList}
|
||||
data={commandPreview}
|
||||
renderItem={this.renderCommandPreviewItem}
|
||||
keyExtractor={item => item.id}
|
||||
keyboardShouldPersistTaps='always'
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderReplyPreview = () => {
|
||||
const {
|
||||
message, replying, replyCancel, user, getCustomEmoji
|
||||
} = this.props;
|
||||
if (!replying) {
|
||||
return null;
|
||||
}
|
||||
return <ReplyPreview key='reply-preview' message={message} close={replyCancel} username={user.username} getCustomEmoji={getCustomEmoji} />;
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { recording, showEmojiKeyboard, showSend } = this.state;
|
||||
const { editing } = this.props;
|
||||
const {
|
||||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||
} = this.state;
|
||||
const {
|
||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme
|
||||
} = this.props;
|
||||
|
||||
const isAndroidTablet = isTablet && isAndroid ? {
|
||||
multiline: false,
|
||||
onSubmitEditing: this.submit,
|
||||
returnKeyType: 'send'
|
||||
} : {};
|
||||
|
||||
if (recording) {
|
||||
return (<Recording onFinish={this.finishAudioMessage} />);
|
||||
return <Recording theme={theme} onFinish={this.finishAudioMessage} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{this.renderCommandPreview()}
|
||||
{this.renderMentions()}
|
||||
<View style={styles.composer} key='messagebox'>
|
||||
{this.renderReplyPreview()}
|
||||
<CommandsPreview commandPreview={commandPreview} showCommandPreview={showCommandPreview} />
|
||||
<Mentions mentions={mentions} trackingType={trackingType} theme={theme} />
|
||||
<View style={[styles.composer, { borderTopColor: themes[theme].separatorColor }]}>
|
||||
<ReplyPreview
|
||||
message={message}
|
||||
close={replyCancel}
|
||||
username={user.username}
|
||||
replying={replying}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<View
|
||||
style={[styles.textArea, editing && styles.editing]}
|
||||
style={[
|
||||
styles.textArea,
|
||||
{ backgroundColor: themes[theme].messageboxBackground }, editing && { backgroundColor: themes[theme].chatComponentBackground }
|
||||
]}
|
||||
testID='messagebox'
|
||||
>
|
||||
<LeftButtons
|
||||
theme={theme}
|
||||
showEmojiKeyboard={showEmojiKeyboard}
|
||||
editing={editing}
|
||||
showFileActions={this.showFileActions}
|
||||
|
@ -918,10 +820,12 @@ class MessageBox extends Component {
|
|||
underlineColorAndroid='transparent'
|
||||
defaultValue=''
|
||||
multiline
|
||||
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
|
||||
testID='messagebox-input'
|
||||
theme={theme}
|
||||
{...isAndroidTablet}
|
||||
/>
|
||||
<RightButtons
|
||||
theme={theme}
|
||||
showSend={showSend}
|
||||
submit={this.submit}
|
||||
recordAudioMessage={this.recordAudioMessage}
|
||||
|
@ -936,8 +840,16 @@ class MessageBox extends Component {
|
|||
render() {
|
||||
console.count(`${ this.constructor.name }.render calls`);
|
||||
const { showEmojiKeyboard, file } = this.state;
|
||||
const { user, baseUrl, theme } = this.props;
|
||||
return (
|
||||
<>
|
||||
<MessageboxContext.Provider
|
||||
value={{
|
||||
user,
|
||||
baseUrl,
|
||||
onPressMention: this.onPressMention,
|
||||
onPressCommandPreview: this.onPressCommandPreview
|
||||
}}
|
||||
>
|
||||
<KeyboardAccessoryView
|
||||
renderContent={this.renderContent}
|
||||
kbInputRef={this.component}
|
||||
|
@ -948,6 +860,7 @@ class MessageBox extends Component {
|
|||
// revealKeyboardInteractive
|
||||
requiresSameParentToManageScrollView
|
||||
addBottomView
|
||||
bottomViewColor={themes[theme].messageboxBackground}
|
||||
/>
|
||||
<UploadModal
|
||||
isVisible={(file && file.isVisible)}
|
||||
|
@ -955,7 +868,7 @@ class MessageBox extends Component {
|
|||
close={() => this.setState({ file: {} })}
|
||||
submit={this.sendMediaMessage}
|
||||
/>
|
||||
</>
|
||||
</MessageboxContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -976,4 +889,4 @@ const dispatchToProps = ({
|
|||
typing: (rid, status) => userTypingAction(rid, status)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);
|
||||
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withTheme(MessageBox));
|
||||
|
|
|
@ -2,33 +2,25 @@ import { StyleSheet } from 'react-native';
|
|||
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BORDER, COLOR_SEPARATOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_PRIMARY
|
||||
} from '../../constants/colors';
|
||||
|
||||
const MENTION_HEIGHT = 50;
|
||||
const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT;
|
||||
|
||||
export default StyleSheet.create({
|
||||
textBox: {
|
||||
backgroundColor: COLOR_WHITE,
|
||||
flex: 0,
|
||||
alignItems: 'center',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: COLOR_SEPARATOR,
|
||||
zIndex: 2
|
||||
},
|
||||
composer: {
|
||||
backgroundColor: COLOR_WHITE,
|
||||
flexDirection: 'column',
|
||||
borderTopColor: COLOR_SEPARATOR,
|
||||
borderTopWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
textArea: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexGrow: 0,
|
||||
backgroundColor: COLOR_WHITE
|
||||
flexGrow: 0
|
||||
},
|
||||
textBoxInput: {
|
||||
textAlignVertical: 'center',
|
||||
|
@ -42,12 +34,8 @@ export default StyleSheet.create({
|
|||
paddingRight: 0,
|
||||
fontSize: 17,
|
||||
letterSpacing: 0,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
editing: {
|
||||
backgroundColor: '#fff5df'
|
||||
},
|
||||
actionButton: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
@ -59,9 +47,7 @@ export default StyleSheet.create({
|
|||
},
|
||||
mentionItem: {
|
||||
height: MENTION_HEIGHT,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: COLOR_BORDER,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 5
|
||||
|
@ -81,30 +67,17 @@ export default StyleSheet.create({
|
|||
textAlign: 'center',
|
||||
width: 46,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textBold,
|
||||
...sharedStyles.textColorNormal
|
||||
...sharedStyles.textBold
|
||||
},
|
||||
mentionText: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorNormal
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
emojiKeyboardContainer: {
|
||||
flex: 1,
|
||||
borderTopColor: COLOR_BORDER,
|
||||
borderTopWidth: 1
|
||||
},
|
||||
iphoneXArea: {
|
||||
height: 50,
|
||||
backgroundColor: COLOR_WHITE,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0
|
||||
borderTopWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
slash: {
|
||||
color: COLOR_PRIMARY,
|
||||
backgroundColor: COLOR_BORDER,
|
||||
height: 30,
|
||||
width: 30,
|
||||
padding: 5,
|
||||
|
@ -120,7 +93,6 @@ export default StyleSheet.create({
|
|||
borderRadius: 4
|
||||
},
|
||||
commandPreview: {
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
height: 100,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
|
|
|
@ -6,11 +6,13 @@ import RocketChat from '../lib/rocketchat';
|
|||
import database from '../lib/database';
|
||||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
|
||||
class MessageErrorActions extends React.Component {
|
||||
static propTypes = {
|
||||
actionsHide: PropTypes.func.isRequired,
|
||||
message: PropTypes.object
|
||||
message: PropTypes.object,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
|
@ -27,17 +29,66 @@ class MessageErrorActions extends React.Component {
|
|||
}
|
||||
|
||||
handleResend = protectedFunction(async() => {
|
||||
const { message } = this.props;
|
||||
await RocketChat.resendMessage(message);
|
||||
const { message, tmid } = this.props;
|
||||
await RocketChat.resendMessage(message, tmid);
|
||||
});
|
||||
|
||||
handleDelete = protectedFunction(async() => {
|
||||
const { message } = this.props;
|
||||
const db = database.active;
|
||||
await db.action(async() => {
|
||||
await message.destroyPermanently();
|
||||
});
|
||||
})
|
||||
handleDelete = async() => {
|
||||
try {
|
||||
const { message, tmid } = this.props;
|
||||
const db = database.active;
|
||||
const deleteBatch = [];
|
||||
const msgCollection = db.collections.get('messages');
|
||||
const threadCollection = db.collections.get('threads');
|
||||
|
||||
// Delete the object (it can be Message or ThreadMessage instance)
|
||||
deleteBatch.push(message.prepareDestroyPermanently());
|
||||
|
||||
// If it's a thread, we find and delete the whole tree, if necessary
|
||||
if (tmid) {
|
||||
try {
|
||||
const msg = await msgCollection.find(message.id);
|
||||
deleteBatch.push(msg.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the thread header and update it
|
||||
const msg = await msgCollection.find(tmid);
|
||||
if (msg.tcount <= 1) {
|
||||
deleteBatch.push(
|
||||
msg.prepareUpdate((m) => {
|
||||
m.tcount = null;
|
||||
m.tlm = null;
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
// If the whole thread was removed, delete the thread
|
||||
const thread = await threadCollection.find(tmid);
|
||||
deleteBatch.push(thread.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
// Do nothing: thread not found
|
||||
}
|
||||
} else {
|
||||
deleteBatch.push(
|
||||
msg.prepareUpdate((m) => {
|
||||
m.tcount -= 1;
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
}
|
||||
await db.action(async() => {
|
||||
await db.batch(...deleteBatch);
|
||||
});
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
|
|
|
@ -10,7 +10,8 @@ import Emoji from './message/Emoji';
|
|||
import I18n from '../i18n';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_WHITE } from '../constants/colors';
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
|
@ -18,18 +19,15 @@ const styles = StyleSheet.create({
|
|||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactCount: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
peopleReacted: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
|
@ -54,15 +52,14 @@ const styles = StyleSheet.create({
|
|||
closeButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: COLOR_WHITE
|
||||
top: 10
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
const customEmojiStyle = { width: 20, height: 20 };
|
||||
|
||||
const Item = React.memo(({
|
||||
item, user, baseUrl, getCustomEmoji
|
||||
item, user, baseUrl, getCustomEmoji, theme
|
||||
}) => {
|
||||
const count = item.usernames.length;
|
||||
let usernames = item.usernames.slice(0, 3)
|
||||
|
@ -84,27 +81,29 @@ const Item = React.memo(({
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.peopleItemContainer}>
|
||||
<Text style={styles.reactCount}>
|
||||
<Text style={[styles.reactCount, { color: themes[theme].buttonText }]}>
|
||||
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
|
||||
</Text>
|
||||
<Text style={styles.peopleReacted}>{ usernames }</Text>
|
||||
<Text style={[styles.peopleReacted, { color: themes[theme].buttonText }]}>{ usernames }</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const ModalContent = React.memo(({ message, onClose, ...props }) => {
|
||||
const ModalContent = React.memo(({
|
||||
message, onClose, ...props
|
||||
}) => {
|
||||
if (message && message.reactions) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<Touchable onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<CustomIcon
|
||||
style={styles.closeButton}
|
||||
style={[styles.closeButton, { color: themes[props.theme].buttonText }]}
|
||||
name='cross'
|
||||
size={20}
|
||||
/>
|
||||
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
|
||||
<Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
<FlatList
|
||||
|
@ -119,7 +118,9 @@ const ModalContent = React.memo(({ message, onClose, ...props }) => {
|
|||
return null;
|
||||
});
|
||||
|
||||
const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
|
||||
const ReactionsModal = React.memo(({
|
||||
isVisible, onClose, theme, ...props
|
||||
}) => (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={onClose}
|
||||
|
@ -128,19 +129,21 @@ const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
|
|||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'left', 'right', 'down']}
|
||||
>
|
||||
<ModalContent onClose={onClose} {...props} />
|
||||
<ModalContent onClose={onClose} theme={theme} {...props} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.theme === nextProps.theme);
|
||||
|
||||
ReactionsModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
onClose: PropTypes.func
|
||||
onClose: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
ReactionsModal.displayName = 'ReactionsModal';
|
||||
|
||||
ModalContent.propTypes = {
|
||||
message: PropTypes.object,
|
||||
onClose: PropTypes.func
|
||||
onClose: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
ModalContent.displayName = 'ReactionsModalContent';
|
||||
|
||||
|
@ -148,8 +151,9 @@ Item.propTypes = {
|
|||
item: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Item.displayName = 'ReactionsModalItem';
|
||||
|
||||
export default ReactionsModal;
|
||||
export default withTheme(ReactionsModal);
|
||||
|
|
|
@ -2,43 +2,42 @@ import React from 'react';
|
|||
import { Image, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { COLOR_TEXT_DESCRIPTION } from '../constants/colors';
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
style: {
|
||||
marginRight: 7,
|
||||
marginTop: 3
|
||||
},
|
||||
imageColor: {
|
||||
tintColor: COLOR_TEXT_DESCRIPTION
|
||||
},
|
||||
iconColor: {
|
||||
color: COLOR_TEXT_DESCRIPTION
|
||||
},
|
||||
discussion: {
|
||||
marginRight: 6
|
||||
}
|
||||
});
|
||||
|
||||
const RoomTypeIcon = React.memo(({ type, size, style }) => {
|
||||
const RoomTypeIcon = React.memo(({
|
||||
type, size, style, theme
|
||||
}) => {
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const color = themes[theme].auxiliaryText;
|
||||
|
||||
if (type === 'discussion') {
|
||||
// FIXME: These are temporary only. We should have all room icons on <Customicon />, but our design team is still working on this.
|
||||
return <CustomIcon name='chat' size={13} style={[styles.style, styles.iconColor, styles.discussion]} />;
|
||||
return <CustomIcon name='chat' size={13} style={[styles.style, styles.iconColor, styles.discussion, { color }]} />;
|
||||
}
|
||||
|
||||
if (type === 'c') {
|
||||
return <Image source={{ uri: 'hashtag' }} style={[styles.style, styles.imageColor, style, { width: size, height: size }]} />;
|
||||
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
|
||||
} if (type === 'd') {
|
||||
return <CustomIcon name='at' size={13} style={[styles.style, styles.iconColor, styles.discussion]} />;
|
||||
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
||||
}
|
||||
return <Image source={{ uri: 'lock' }} style={[styles.style, styles.imageColor, style, { width: size, height: size }]} />;
|
||||
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
|
||||
});
|
||||
|
||||
RoomTypeIcon.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
style: PropTypes.object
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, StyleSheet, TextInput, Text
|
||||
} from 'react-native';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import TextInput from '../presentation/TextInput';
|
||||
import I18n from '../i18n';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { withTheme } from '../theme';
|
||||
import { themes } from '../constants/colors';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: isIOS ? '#F7F8FA' : '#54585E',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1
|
||||
},
|
||||
searchBox: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#E1E5E8',
|
||||
borderRadius: 10,
|
||||
color: '#8E8E93',
|
||||
flexDirection: 'row',
|
||||
fontSize: 17,
|
||||
height: 36,
|
||||
|
@ -31,7 +29,6 @@ const styles = StyleSheet.create({
|
|||
flex: 1
|
||||
},
|
||||
input: {
|
||||
color: '#8E8E93',
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
marginLeft: 8,
|
||||
|
@ -44,24 +41,29 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
cancelText: {
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorHeaderBack,
|
||||
fontSize: 17
|
||||
}
|
||||
});
|
||||
|
||||
const CancelButton = onCancelPress => (
|
||||
const CancelButton = (onCancelPress, theme) => (
|
||||
<Touchable onPress={onCancelPress} style={styles.cancel}>
|
||||
<Text style={styles.cancelText}>{I18n.t('Cancel')}</Text>
|
||||
<Text style={[styles.cancelText, { color: themes[theme].tintColor }]}>{I18n.t('Cancel')}</Text>
|
||||
</Touchable>
|
||||
);
|
||||
|
||||
const SearchBox = ({
|
||||
onChangeText, onSubmitEditing, testID, hasCancel, onCancelPress, ...props
|
||||
onChangeText, onSubmitEditing, testID, hasCancel, onCancelPress, inputRef, theme, ...props
|
||||
}) => (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.searchBox}>
|
||||
<CustomIcon name='magnifier' size={14} color='#8E8E93' />
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: isIOS ? themes[theme].headerBackground : themes[theme].headerSecondaryBackground }
|
||||
]}
|
||||
>
|
||||
<View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
|
||||
<CustomIcon name='magnifier' size={14} color={themes[theme].auxiliaryText} />
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
blurOnSubmit
|
||||
|
@ -73,10 +75,11 @@ const SearchBox = ({
|
|||
underlineColorAndroid='transparent'
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
theme={theme}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
{ hasCancel ? CancelButton(onCancelPress) : null }
|
||||
{ hasCancel ? CancelButton(onCancelPress, theme) : null }
|
||||
</View>
|
||||
);
|
||||
|
||||
|
@ -85,7 +88,9 @@ SearchBox.propTypes = {
|
|||
onSubmitEditing: PropTypes.func,
|
||||
hasCancel: PropTypes.bool,
|
||||
onCancelPress: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
inputRef: PropTypes.func,
|
||||
testID: PropTypes.string
|
||||
};
|
||||
|
||||
export default SearchBox;
|
||||
export default withTheme(SearchBox);
|
||||
|
|
|
@ -2,20 +2,28 @@ import React from 'react';
|
|||
import { View, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COLOR_SEPARATOR } from '../constants/colors';
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
separator: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: COLOR_SEPARATOR
|
||||
height: StyleSheet.hairlineWidth
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const Separator = React.memo(({ style }) => <View style={[styles.separator, style]} />);
|
||||
const Separator = React.memo(({ style, theme }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.separator,
|
||||
style,
|
||||
{ backgroundColor: themes[theme].separatorColor }
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
Separator.propTypes = {
|
||||
style: PropTypes.object
|
||||
style: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Separator;
|
||||
|
|
|
@ -3,23 +3,19 @@ import { StatusBar as StatusBarRN } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { HEADER_BACKGROUND, COLOR_WHITE } from '../constants/colors';
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
|
||||
const HEADER_BAR_STYLE = isIOS ? 'dark-content' : 'light-content';
|
||||
|
||||
const StatusBar = React.memo(({ light }) => {
|
||||
if (light) {
|
||||
return <StatusBarRN backgroundColor={COLOR_WHITE} barStyle='dark-content' animated />;
|
||||
const StatusBar = React.memo(({ theme }) => {
|
||||
let barStyle = 'light-content';
|
||||
if (theme === 'light' && isIOS) {
|
||||
barStyle = 'dark-content';
|
||||
}
|
||||
return <StatusBarRN backgroundColor={HEADER_BACKGROUND} barStyle={HEADER_BAR_STYLE} animated />;
|
||||
return <StatusBarRN backgroundColor={themes[theme].headerBackground} barStyle={barStyle} animated />;
|
||||
});
|
||||
|
||||
StatusBar.propTypes = {
|
||||
light: PropTypes.bool
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
StatusBar.defaultProps = {
|
||||
light: false
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
export default withTheme(StatusBar);
|
||||
|
|
|
@ -1,37 +1,34 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, StyleSheet, Text, TextInput
|
||||
} from 'react-native';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
|
||||
import sharedStyles from '../views/Styles';
|
||||
import {
|
||||
COLOR_DANGER, COLOR_TEXT_DESCRIPTION, COLOR_TEXT, COLOR_BORDER
|
||||
} from '../constants/colors';
|
||||
import TextInput from '../presentation/TextInput';
|
||||
import { themes } from '../constants/colors';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
textAlign: 'center',
|
||||
paddingTop: 5
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 10
|
||||
},
|
||||
label: {
|
||||
marginBottom: 10,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textSemibold,
|
||||
...sharedStyles.textColorNormal
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
input: {
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorNormal,
|
||||
height: 48,
|
||||
fontSize: 16,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
borderWidth: 1,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'white',
|
||||
borderColor: COLOR_BORDER
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 2
|
||||
},
|
||||
inputIconLeft: {
|
||||
paddingLeft: 45
|
||||
|
@ -39,13 +36,6 @@ const styles = StyleSheet.create({
|
|||
inputIconRight: {
|
||||
paddingRight: 45
|
||||
},
|
||||
labelError: {
|
||||
color: COLOR_DANGER
|
||||
},
|
||||
inputError: {
|
||||
color: COLOR_DANGER,
|
||||
borderColor: COLOR_DANGER
|
||||
},
|
||||
wrap: {
|
||||
position: 'relative'
|
||||
},
|
||||
|
@ -58,12 +48,6 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
iconRight: {
|
||||
right: 15
|
||||
},
|
||||
icon: {
|
||||
color: COLOR_TEXT
|
||||
},
|
||||
password: {
|
||||
color: COLOR_TEXT_DESCRIPTION
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -78,11 +62,13 @@ export default class RCTextInput extends React.PureComponent {
|
|||
inputRef: PropTypes.func,
|
||||
testID: PropTypes.string,
|
||||
iconLeft: PropTypes.string,
|
||||
placeholder: PropTypes.string
|
||||
placeholder: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
error: {}
|
||||
error: {},
|
||||
theme: 'light'
|
||||
}
|
||||
|
||||
state = {
|
||||
|
@ -90,12 +76,12 @@ export default class RCTextInput extends React.PureComponent {
|
|||
}
|
||||
|
||||
get iconLeft() {
|
||||
const { testID, iconLeft } = this.props;
|
||||
const { testID, iconLeft, theme } = this.props;
|
||||
return (
|
||||
<CustomIcon
|
||||
name={iconLeft}
|
||||
testID={testID ? `${ testID }-icon-left` : null}
|
||||
style={[styles.iconContainer, styles.iconLeft, styles.icon]}
|
||||
style={[styles.iconContainer, styles.iconLeft, { color: themes[theme].bodyText }]}
|
||||
size={20}
|
||||
/>
|
||||
);
|
||||
|
@ -103,13 +89,13 @@ export default class RCTextInput extends React.PureComponent {
|
|||
|
||||
get iconPassword() {
|
||||
const { showPassword } = this.state;
|
||||
const { testID } = this.props;
|
||||
const { testID, theme } = this.props;
|
||||
return (
|
||||
<BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
|
||||
<CustomIcon
|
||||
name={showPassword ? 'Eye' : 'eye-off'}
|
||||
testID={testID ? `${ testID }-icon-right` : null}
|
||||
style={[styles.icon, styles.password]}
|
||||
style={{ color: themes[theme].auxiliaryText }}
|
||||
size={20}
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
@ -123,19 +109,40 @@ export default class RCTextInput extends React.PureComponent {
|
|||
render() {
|
||||
const { showPassword } = this.state;
|
||||
const {
|
||||
label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, ...inputProps
|
||||
label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
|
||||
} = this.props;
|
||||
const { dangerColor } = themes[theme];
|
||||
return (
|
||||
<View style={[styles.inputContainer, containerStyle]}>
|
||||
{label ? <Text contentDescription={null} accessibilityLabel={null} style={[styles.label, error.error && styles.labelError]}>{label}</Text> : null}
|
||||
{label ? (
|
||||
<Text
|
||||
contentDescription={null}
|
||||
accessibilityLabel={null}
|
||||
style={[
|
||||
styles.label,
|
||||
{ color: themes[theme].titleText },
|
||||
error.error && { color: dangerColor }
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
) : null}
|
||||
<View style={styles.wrap}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
error.error && styles.inputError,
|
||||
inputStyle,
|
||||
error.error && {
|
||||
color: dangerColor,
|
||||
borderColor: dangerColor
|
||||
},
|
||||
iconLeft && styles.inputIconLeft,
|
||||
secureTextEntry && styles.inputIconRight
|
||||
secureTextEntry && styles.inputIconRight,
|
||||
{
|
||||
backgroundColor: themes[theme].backgroundColor,
|
||||
borderColor: themes[theme].separatorColor,
|
||||
color: themes[theme].titleText
|
||||
},
|
||||
inputStyle
|
||||
]}
|
||||
ref={inputRef}
|
||||
autoCorrect={false}
|
||||
|
@ -145,14 +152,14 @@ export default class RCTextInput extends React.PureComponent {
|
|||
testID={testID}
|
||||
accessibilityLabel={placeholder}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
|
||||
contentDescription={placeholder}
|
||||
theme={theme}
|
||||
{...inputProps}
|
||||
/>
|
||||
{iconLeft ? this.iconLeft : null}
|
||||
{secureTextEntry ? this.iconPassword : null}
|
||||
</View>
|
||||
{error.error ? <Text style={sharedStyles.error}>{error.reason}</Text> : null}
|
||||
{error.error ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import EasyToast from 'react-native-easy-toast';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COLOR_TOAST, COLOR_WHITE } from '../constants/colors';
|
||||
import { themes } from '../constants/colors';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import EventEmitter from '../utils/events';
|
||||
import { withTheme } from '../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
toast: {
|
||||
backgroundColor: COLOR_TOAST,
|
||||
maxWidth: 300,
|
||||
padding: 10
|
||||
},
|
||||
text: {
|
||||
...sharedStyles.textRegular,
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
textAlign: 'center'
|
||||
}
|
||||
|
@ -22,12 +22,20 @@ const styles = StyleSheet.create({
|
|||
|
||||
export const LISTENER = 'Toast';
|
||||
|
||||
export default class Toast extends React.Component {
|
||||
class Toast extends React.Component {
|
||||
static propTypes = {
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.addEventListener(LISTENER, this.showToast);
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -40,14 +48,17 @@ export default class Toast extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<EasyToast
|
||||
ref={toast => this.toast = toast}
|
||||
position='center'
|
||||
style={styles.toast}
|
||||
textStyle={styles.text}
|
||||
style={[styles.toast, { backgroundColor: themes[theme].toastBackground }]}
|
||||
textStyle={[styles.text, { color: themes[theme].buttonText }]}
|
||||
opacity={0.9}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Toast);
|
||||
|
|
|
@ -2,12 +2,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const AtMention = React.memo(({
|
||||
mention, mentions, username, navToRoomInfo, preview, style = []
|
||||
mention, mentions, username, navToRoomInfo, preview, style = [], theme
|
||||
}) => {
|
||||
let mentionStyle = styles.mention;
|
||||
let mentionStyle = { ...styles.mention, color: themes[theme].buttonText };
|
||||
if (mention === 'all' || mention === 'here') {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
|
@ -16,7 +18,12 @@ const AtMention = React.memo(({
|
|||
} else if (mention === username) {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
...styles.mentionLoggedUser
|
||||
backgroundColor: themes[theme].actionTintColor
|
||||
};
|
||||
} else {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
color: themes[theme].actionTintColor
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,7 +40,7 @@ const AtMention = React.memo(({
|
|||
|
||||
return (
|
||||
<Text
|
||||
style={[preview ? styles.text : mentionStyle, ...style]}
|
||||
style={[preview ? { ...styles.text, color: themes[theme].titleText } : mentionStyle, ...style]}
|
||||
onPress={preview ? undefined : handlePress}
|
||||
>
|
||||
{`@${ mention }`}
|
||||
|
@ -47,6 +54,7 @@ AtMention.propTypes = {
|
|||
navToRoomInfo: PropTypes.func,
|
||||
style: PropTypes.array,
|
||||
preview: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
};
|
||||
|
||||
|
|
|
@ -2,11 +2,13 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const BlockQuote = React.memo(({ children }) => (
|
||||
const BlockQuote = React.memo(({ children, theme }) => (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.quote} />
|
||||
<View style={[styles.quote, { backgroundColor: themes[theme].borderColor }]} />
|
||||
<View style={styles.childContainer}>
|
||||
{children}
|
||||
</View>
|
||||
|
@ -14,7 +16,8 @@ const BlockQuote = React.memo(({ children }) => (
|
|||
));
|
||||
|
||||
BlockQuote.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default BlockQuote;
|
||||
|
|
|
@ -4,11 +4,12 @@ import { Text } from 'react-native';
|
|||
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const Emoji = React.memo(({
|
||||
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis, style = []
|
||||
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis, style = [], theme
|
||||
}) => {
|
||||
const emojiUnicode = shortnameToUnicode(literal);
|
||||
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
|
||||
|
@ -24,6 +25,7 @@ const Emoji = React.memo(({
|
|||
return (
|
||||
<Text
|
||||
style={[
|
||||
{ color: themes[theme].titleText },
|
||||
isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
|
||||
...style
|
||||
]}
|
||||
|
@ -40,7 +42,8 @@ Emoji.propTypes = {
|
|||
getCustomEmoji: PropTypes.func,
|
||||
baseUrl: PropTypes.string,
|
||||
customEmojis: PropTypes.bool,
|
||||
style: PropTypes.array
|
||||
style: PropTypes.array,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Emoji;
|
||||
|
|
|
@ -2,10 +2,12 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const Hashtag = React.memo(({
|
||||
hashtag, channels, navToRoomInfo, preview, style = []
|
||||
hashtag, channels, navToRoomInfo, preview, style = [], theme
|
||||
}) => {
|
||||
const handlePress = () => {
|
||||
const index = channels.findIndex(channel => channel.name === hashtag);
|
||||
|
@ -19,14 +21,18 @@ const Hashtag = React.memo(({
|
|||
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
|
||||
return (
|
||||
<Text
|
||||
style={[preview ? styles.text : styles.mention, ...style]}
|
||||
style={[preview ? { ...styles.text, color: themes[theme].titleText } : styles.mention, ...style]}
|
||||
onPress={preview ? undefined : handlePress}
|
||||
>
|
||||
{`#${ hashtag }`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return `#${ hashtag }`;
|
||||
return (
|
||||
<Text style={[preview ? { ...styles.text, color: themes[theme].titleText } : styles.mention, ...style]}>
|
||||
{`#${ hashtag }`}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
|
||||
Hashtag.propTypes = {
|
||||
|
@ -34,6 +40,7 @@ Hashtag.propTypes = {
|
|||
navToRoomInfo: PropTypes.func,
|
||||
style: PropTypes.array,
|
||||
preview: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,17 @@ import PropTypes from 'prop-types';
|
|||
import { Text } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import openLink from '../../utils/openLink';
|
||||
|
||||
const Link = React.memo(({
|
||||
children, link, preview
|
||||
children, link, preview, theme
|
||||
}) => {
|
||||
const handlePress = () => {
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
openLink(link);
|
||||
openLink(link, theme);
|
||||
};
|
||||
|
||||
const childLength = React.Children.toArray(children).filter(o => o).length;
|
||||
|
@ -21,7 +22,11 @@ const Link = React.memo(({
|
|||
return (
|
||||
<Text
|
||||
onPress={preview ? undefined : handlePress}
|
||||
style={styles.link}
|
||||
style={
|
||||
!preview
|
||||
? { ...styles.link, color: themes[theme].actionTintColor }
|
||||
: { color: themes[theme].titleText }
|
||||
}
|
||||
>
|
||||
{ childLength !== 0 ? children : link }
|
||||
</Text>
|
||||
|
@ -31,6 +36,7 @@ const Link = React.memo(({
|
|||
Link.propTypes = {
|
||||
children: PropTypes.node,
|
||||
link: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
preview: PropTypes.bool
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
|
@ -21,7 +23,7 @@ const style = StyleSheet.create({
|
|||
});
|
||||
|
||||
const ListItem = React.memo(({
|
||||
children, level, bulletWidth, continue: _continue, ordered, index
|
||||
children, level, bulletWidth, continue: _continue, ordered, index, theme
|
||||
}) => {
|
||||
let bullet;
|
||||
if (_continue) {
|
||||
|
@ -37,7 +39,7 @@ const ListItem = React.memo(({
|
|||
return (
|
||||
<View style={style.container}>
|
||||
<View style={[{ width: bulletWidth }, style.bullet]}>
|
||||
<Text>
|
||||
<Text style={{ color: themes[theme].titleText }}>
|
||||
{bullet}
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -54,6 +56,7 @@ ListItem.propTypes = {
|
|||
level: PropTypes.number,
|
||||
ordered: PropTypes.bool,
|
||||
continue: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
index: PropTypes.number
|
||||
};
|
||||
|
||||
|
|
|
@ -11,16 +11,17 @@ import { CELL_WIDTH } from './TableCell';
|
|||
import styles from './styles';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import I18n from '../../i18n';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const MAX_HEIGHT = 300;
|
||||
|
||||
const Table = React.memo(({
|
||||
children, numColumns
|
||||
children, numColumns, theme
|
||||
}) => {
|
||||
const getTableWidth = () => numColumns * CELL_WIDTH;
|
||||
|
||||
const renderRows = (drawExtraBorders = true) => {
|
||||
const tableStyle = [styles.table];
|
||||
const tableStyle = [styles.table, { borderColor: themes[theme].borderColor }];
|
||||
if (drawExtraBorders) {
|
||||
tableStyle.push(styles.tableExtraBorders);
|
||||
}
|
||||
|
@ -45,18 +46,19 @@ const Table = React.memo(({
|
|||
contentContainerStyle={{ width: getTableWidth() }}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[styles.containerTable, { maxWidth: getTableWidth(), maxHeight: MAX_HEIGHT }]}
|
||||
style={[styles.containerTable, { maxWidth: getTableWidth(), maxHeight: MAX_HEIGHT, borderColor: themes[theme].borderColor }]}
|
||||
>
|
||||
{renderRows(false)}
|
||||
</ScrollView>
|
||||
<Text style={styles.textInfo}>{I18n.t('Full_table')}</Text>
|
||||
<Text style={[styles.textInfo, { color: themes[theme].auxiliaryText }]}>{I18n.t('Full_table')}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
Table.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
numColumns: PropTypes.number.isRequired
|
||||
numColumns: PropTypes.number.isRequired,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
|
|
@ -2,14 +2,16 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
export const CELL_WIDTH = 100;
|
||||
|
||||
const TableCell = React.memo(({
|
||||
isLastCell, align, children
|
||||
isLastCell, align, children, theme
|
||||
}) => {
|
||||
const cellStyle = [styles.cell];
|
||||
const cellStyle = [styles.cell, { borderColor: themes[theme].borderColor }];
|
||||
if (!isLastCell) {
|
||||
cellStyle.push(styles.cellRightBorder);
|
||||
}
|
||||
|
@ -23,7 +25,7 @@ const TableCell = React.memo(({
|
|||
|
||||
return (
|
||||
<View style={[...cellStyle, { width: CELL_WIDTH }]}>
|
||||
<Text style={textStyle}>
|
||||
<Text style={[textStyle, { color: themes[theme].titleText }]}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -33,7 +35,8 @@ const TableCell = React.memo(({
|
|||
TableCell.propTypes = {
|
||||
align: PropTypes.oneOf(['', 'left', 'center', 'right']),
|
||||
children: PropTypes.node,
|
||||
isLastCell: PropTypes.bool
|
||||
isLastCell: PropTypes.bool,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default TableCell;
|
||||
|
|
|
@ -2,12 +2,14 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const TableRow = React.memo(({
|
||||
isLastRow, children: _children
|
||||
isLastRow, children: _children, theme
|
||||
}) => {
|
||||
const rowStyle = [styles.row];
|
||||
const rowStyle = [styles.row, { borderColor: themes[theme].borderColor }];
|
||||
if (!isLastRow) {
|
||||
rowStyle.push(styles.rowBottomBorder);
|
||||
}
|
||||
|
@ -22,7 +24,8 @@ const TableRow = React.memo(({
|
|||
|
||||
TableRow.propTypes = {
|
||||
children: PropTypes.node,
|
||||
isLastRow: PropTypes.bool
|
||||
isLastRow: PropTypes.bool,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default TableRow;
|
||||
|
|
|
@ -3,9 +3,10 @@ import { Text, Image } from 'react-native';
|
|||
import { Parser, Node } from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toShort, shortnameToUnicode } from 'emoji-toolkit';
|
||||
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
import MarkdownLink from './Link';
|
||||
import MarkdownList from './List';
|
||||
|
@ -32,13 +33,19 @@ const emojiRanges = [
|
|||
' |\n' // allow spaces and line breaks
|
||||
].join('|');
|
||||
|
||||
const removeSpaces = str => str && str.replace(/\s/g, '');
|
||||
|
||||
const removeAllEmoji = str => str.replace(new RegExp(emojiRanges, 'g'), '');
|
||||
|
||||
const isOnlyEmoji = str => !removeAllEmoji(str).length;
|
||||
const isOnlyEmoji = (str) => {
|
||||
str = removeSpaces(str);
|
||||
return !removeAllEmoji(str).length;
|
||||
};
|
||||
|
||||
const removeOneEmoji = str => str.replace(new RegExp(emojiRanges), '');
|
||||
|
||||
const emojiCount = (str) => {
|
||||
str = removeSpaces(str);
|
||||
let oldLength = 0;
|
||||
let counter = 0;
|
||||
|
||||
|
@ -53,7 +60,7 @@ const emojiCount = (str) => {
|
|||
return counter;
|
||||
};
|
||||
|
||||
export default class Markdown extends PureComponent {
|
||||
class Markdown extends PureComponent {
|
||||
static propTypes = {
|
||||
msg: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
|
@ -68,6 +75,7 @@ export default class Markdown extends PureComponent {
|
|||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
navToRoomInfo: PropTypes.func,
|
||||
preview: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
style: PropTypes.array
|
||||
};
|
||||
|
||||
|
@ -134,7 +142,9 @@ export default class Markdown extends PureComponent {
|
|||
};
|
||||
|
||||
renderText = ({ context, literal }) => {
|
||||
const { numberOfLines, preview, style = [] } = this.props;
|
||||
const {
|
||||
numberOfLines, preview, style = []
|
||||
} = this.props;
|
||||
const defaultStyle = [
|
||||
this.isMessageContainsOnlyEmoji && !preview ? styles.textBig : {},
|
||||
...context.map(type => styles[type])
|
||||
|
@ -154,13 +164,45 @@ export default class Markdown extends PureComponent {
|
|||
}
|
||||
|
||||
renderCodeInline = ({ literal }) => {
|
||||
const { preview } = this.props;
|
||||
return <Text style={!preview ? styles.codeInline : {}}>{literal}</Text>;
|
||||
const { preview, theme, style = [] } = this.props;
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
!preview
|
||||
? {
|
||||
...styles.codeInline,
|
||||
color: themes[theme].titleText,
|
||||
backgroundColor: themes[theme].bannerBackground,
|
||||
borderColor: themes[theme].bannerBackground
|
||||
}
|
||||
: { ...styles.text, color: themes[theme].titleText },
|
||||
...style
|
||||
]}
|
||||
>
|
||||
{literal}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
renderCodeBlock = ({ literal }) => {
|
||||
const { preview } = this.props;
|
||||
return <Text style={!preview ? styles.codeBlock : {}}>{literal}</Text>;
|
||||
const { preview, theme, style = [] } = this.props;
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
!preview
|
||||
? {
|
||||
...styles.codeBlock,
|
||||
color: themes[theme].titleText,
|
||||
backgroundColor: themes[theme].bannerBackground,
|
||||
borderColor: themes[theme].bannerBackground
|
||||
}
|
||||
: { ...styles.text, color: themes[theme].titleText },
|
||||
...style
|
||||
]}
|
||||
>
|
||||
{literal}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
renderBreak = () => {
|
||||
|
@ -169,21 +211,25 @@ export default class Markdown extends PureComponent {
|
|||
}
|
||||
|
||||
renderParagraph = ({ children }) => {
|
||||
const { numberOfLines, style } = this.props;
|
||||
const { numberOfLines, style, theme } = this.props;
|
||||
if (!children || children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Text style={style} numberOfLines={numberOfLines}>
|
||||
<Text style={[style, { color: themes[theme].titleText }]} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
renderLink = ({ children, href }) => {
|
||||
const { preview } = this.props;
|
||||
const { preview, theme } = this.props;
|
||||
return (
|
||||
<MarkdownLink link={href} preview={preview}>
|
||||
<MarkdownLink
|
||||
link={href}
|
||||
preview={preview}
|
||||
theme={theme}
|
||||
>
|
||||
{children}
|
||||
</MarkdownLink>
|
||||
);
|
||||
|
@ -191,7 +237,7 @@ export default class Markdown extends PureComponent {
|
|||
|
||||
renderHashtag = ({ hashtag }) => {
|
||||
const {
|
||||
channels, navToRoomInfo, style, preview
|
||||
channels, navToRoomInfo, style, preview, theme
|
||||
} = this.props;
|
||||
return (
|
||||
<MarkdownHashtag
|
||||
|
@ -199,6 +245,7 @@ export default class Markdown extends PureComponent {
|
|||
channels={channels}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
preview={preview}
|
||||
theme={theme}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
|
@ -206,7 +253,7 @@ export default class Markdown extends PureComponent {
|
|||
|
||||
renderAtMention = ({ mentionName }) => {
|
||||
const {
|
||||
username, mentions, navToRoomInfo, preview, style
|
||||
username, mentions, navToRoomInfo, preview, style, theme
|
||||
} = this.props;
|
||||
return (
|
||||
<MarkdownAtMention
|
||||
|
@ -215,6 +262,7 @@ export default class Markdown extends PureComponent {
|
|||
username={username}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
preview={preview}
|
||||
theme={theme}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
|
@ -222,7 +270,7 @@ export default class Markdown extends PureComponent {
|
|||
|
||||
renderEmoji = ({ emojiName, literal }) => {
|
||||
const {
|
||||
getCustomEmoji, baseUrl, customEmojis = true, preview, style
|
||||
getCustomEmoji, baseUrl, customEmojis = true, preview, style, theme
|
||||
} = this.props;
|
||||
return (
|
||||
<MarkdownEmoji
|
||||
|
@ -233,19 +281,23 @@ export default class Markdown extends PureComponent {
|
|||
baseUrl={baseUrl}
|
||||
customEmojis={customEmojis}
|
||||
style={style}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderImage = ({ src }) => <Image style={styles.inlineImage} source={{ uri: src }} />;
|
||||
|
||||
renderEditedIndicator = () => <Text style={styles.edited}> ({I18n.t('edited')})</Text>;
|
||||
renderEditedIndicator = () => {
|
||||
const { theme } = this.props;
|
||||
return <Text style={[styles.edited, { color: themes[theme].auxiliaryText }]}> ({I18n.t('edited')})</Text>;
|
||||
}
|
||||
|
||||
renderHeading = ({ children, level }) => {
|
||||
const { numberOfLines } = this.props;
|
||||
const { numberOfLines, theme } = this.props;
|
||||
const textStyle = styles[`heading${ level }Text`];
|
||||
return (
|
||||
<Text numberOfLines={numberOfLines} style={textStyle}>
|
||||
<Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].titleText }]}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
@ -270,11 +322,13 @@ export default class Markdown extends PureComponent {
|
|||
renderListItem = ({
|
||||
children, context, ...otherProps
|
||||
}) => {
|
||||
const { theme } = this.props;
|
||||
const level = context.filter(type => type === 'list').length;
|
||||
|
||||
return (
|
||||
<MarkdownListItem
|
||||
level={level}
|
||||
theme={theme}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
@ -283,30 +337,39 @@ export default class Markdown extends PureComponent {
|
|||
};
|
||||
|
||||
renderBlockQuote = ({ children }) => {
|
||||
const { preview } = this.props;
|
||||
const { preview, theme } = this.props;
|
||||
if (preview) {
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<MarkdownBlockQuote>
|
||||
<MarkdownBlockQuote theme={theme}>
|
||||
{children}
|
||||
</MarkdownBlockQuote>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable = ({ children, numColumns }) => (
|
||||
<MarkdownTable numColumns={numColumns}>
|
||||
{children}
|
||||
</MarkdownTable>
|
||||
);
|
||||
renderTable = ({ children, numColumns }) => {
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<MarkdownTable numColumns={numColumns} theme={theme}>
|
||||
{children}
|
||||
</MarkdownTable>
|
||||
);
|
||||
}
|
||||
|
||||
renderTableRow = args => <MarkdownTableRow {...args} />;
|
||||
renderTableRow = (args) => {
|
||||
const { theme } = this.props;
|
||||
return <MarkdownTableRow {...args} theme={theme} />;
|
||||
}
|
||||
|
||||
renderTableCell = args => <MarkdownTableCell {...args} />;
|
||||
renderTableCell = (args) => {
|
||||
const { theme } = this.props;
|
||||
return <MarkdownTableCell {...args} theme={theme} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
msg, useMarkdown = true, numberOfLines, preview = false
|
||||
msg, useMarkdown = true, numberOfLines, preview = false, theme
|
||||
} = this.props;
|
||||
|
||||
if (!msg) {
|
||||
|
@ -322,18 +385,21 @@ export default class Markdown extends PureComponent {
|
|||
|
||||
if (preview) {
|
||||
m = m.split('\n').reduce((lines, line) => `${ lines } ${ line }`, '');
|
||||
const ast = this.parser.parse(m);
|
||||
return this.renderer.render(ast);
|
||||
}
|
||||
|
||||
if (!useMarkdown && !preview) {
|
||||
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
|
||||
return <Text style={[styles.text, { color: themes[theme].titleText }]} numberOfLines={numberOfLines}>{m}</Text>;
|
||||
}
|
||||
|
||||
const ast = this.parser.parse(m);
|
||||
const encodedEmojis = toShort(m);
|
||||
this.isMessageContainsOnlyEmoji = isOnlyEmoji(encodedEmojis) && emojiCount(encodedEmojis) <= 3;
|
||||
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
|
||||
|
||||
this.editedMessage(ast);
|
||||
|
||||
return this.renderer.render(ast);
|
||||
}
|
||||
}
|
||||
|
||||
export default Markdown;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { StyleSheet, Platform } from 'react-native';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER
|
||||
} from '../../constants/colors';
|
||||
|
||||
const codeFontFamily = Platform.select({
|
||||
ios: { fontFamily: 'Courier New' },
|
||||
|
@ -35,18 +32,15 @@ export default StyleSheet.create({
|
|||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
textInfo: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
textBig: {
|
||||
fontSize: 30,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
customEmoji: {
|
||||
|
@ -65,12 +59,7 @@ export default StyleSheet.create({
|
|||
...sharedStyles.textMedium,
|
||||
backgroundColor: '#E8F2FF'
|
||||
},
|
||||
mentionLoggedUser: {
|
||||
color: COLOR_WHITE,
|
||||
backgroundColor: COLOR_PRIMARY
|
||||
},
|
||||
mentionAll: {
|
||||
color: COLOR_WHITE,
|
||||
backgroundColor: '#FF5B5A'
|
||||
},
|
||||
paragraph: {
|
||||
|
@ -90,26 +79,21 @@ export default StyleSheet.create({
|
|||
...sharedStyles.textRegular,
|
||||
...codeFontFamily,
|
||||
borderWidth: 1,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderRadius: 4
|
||||
},
|
||||
codeBlock: {
|
||||
...sharedStyles.textRegular,
|
||||
...codeFontFamily,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
padding: 4
|
||||
},
|
||||
link: {
|
||||
fontSize: 16,
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
edited: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
heading1: {
|
||||
|
@ -139,7 +123,6 @@ export default StyleSheet.create({
|
|||
quote: {
|
||||
height: '100%',
|
||||
width: 2,
|
||||
backgroundColor: COLOR_BORDER,
|
||||
marginRight: 5
|
||||
},
|
||||
touchableTable: {
|
||||
|
@ -147,11 +130,9 @@ export default StyleSheet.create({
|
|||
},
|
||||
containerTable: {
|
||||
borderBottomWidth: 1,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderRightWidth: 1
|
||||
},
|
||||
table: {
|
||||
borderColor: COLOR_BORDER,
|
||||
borderLeftWidth: 1,
|
||||
borderTopWidth: 1
|
||||
},
|
||||
|
@ -163,11 +144,9 @@ export default StyleSheet.create({
|
|||
flexDirection: 'row'
|
||||
},
|
||||
rowBottomBorder: {
|
||||
borderColor: COLOR_BORDER,
|
||||
borderBottomWidth: 1
|
||||
},
|
||||
cell: {
|
||||
borderColor: COLOR_BORDER,
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 13,
|
||||
paddingVertical: 6
|
||||
|
|
|
@ -8,7 +8,7 @@ import Video from './Video';
|
|||
import Reply from './Reply';
|
||||
|
||||
const Attachments = React.memo(({
|
||||
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji, theme
|
||||
}) => {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
|
@ -16,19 +16,19 @@ const Attachments = React.memo(({
|
|||
|
||||
return attachments.map((file, index) => {
|
||||
if (file.image_url) {
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
|
||||
});
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments));
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
|
||||
|
||||
Attachments.propTypes = {
|
||||
attachments: PropTypes.array,
|
||||
|
@ -37,7 +37,8 @@ Attachments.propTypes = {
|
|||
baseUrl: PropTypes.string,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Attachments.displayName = 'MessageAttachments';
|
||||
|
||||
|
|
|
@ -12,8 +12,9 @@ import Touchable from 'react-native-platform-touchable';
|
|||
import Markdown from '../markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { isAndroid, isIOS } from '../../utils/deviceInfo';
|
||||
import { withSplit } from '../../split';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
audioContainer: {
|
||||
|
@ -21,8 +22,6 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: 56,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
marginBottom: 6
|
||||
|
@ -32,16 +31,12 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
playPauseImage: {
|
||||
color: COLOR_PRIMARY
|
||||
},
|
||||
slider: {
|
||||
flex: 1
|
||||
},
|
||||
duration: {
|
||||
marginHorizontal: 12,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
@ -56,29 +51,32 @@ const sliderAnimationConfig = {
|
|||
delay: 0
|
||||
};
|
||||
|
||||
const Button = React.memo(({ paused, onPress }) => (
|
||||
const Button = React.memo(({ paused, onPress, theme }) => (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
<CustomIcon name={paused ? 'play' : 'pause'} size={36} style={styles.playPauseImage} />
|
||||
<CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
Button.propTypes = {
|
||||
paused: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
Button.displayName = 'MessageAudioButton';
|
||||
|
||||
export default class Audio extends React.Component {
|
||||
class Audio extends React.Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
useMarkdown: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
}
|
||||
|
||||
|
@ -97,7 +95,10 @@ export default class Audio extends React.Component {
|
|||
const {
|
||||
currentTime, duration, paused, uri
|
||||
} = this.state;
|
||||
const { file } = this.props;
|
||||
const { file, split, theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.currentTime !== currentTime) {
|
||||
return true;
|
||||
}
|
||||
|
@ -113,6 +114,9 @@ export default class Audio extends React.Component {
|
|||
if (!equal(nextProps.file, file)) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.split !== split) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -153,7 +157,7 @@ export default class Audio extends React.Component {
|
|||
uri, paused, currentTime, duration
|
||||
} = this.state;
|
||||
const {
|
||||
user, baseUrl, file, getCustomEmoji, useMarkdown
|
||||
user, baseUrl, file, getCustomEmoji, useMarkdown, split, theme
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
|
||||
|
@ -163,7 +167,13 @@ export default class Audio extends React.Component {
|
|||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.audioContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.audioContainer,
|
||||
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor },
|
||||
split && sharedStyles.tabletContent
|
||||
]}
|
||||
>
|
||||
<Video
|
||||
ref={this.setRef}
|
||||
source={{ uri }}
|
||||
|
@ -173,7 +183,7 @@ export default class Audio extends React.Component {
|
|||
paused={paused}
|
||||
repeat={false}
|
||||
/>
|
||||
<Button paused={paused} onPress={this.togglePlayPause} />
|
||||
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
|
@ -181,15 +191,18 @@ export default class Audio extends React.Component {
|
|||
minimumValue={0}
|
||||
animateTransitions
|
||||
animationConfig={sliderAnimationConfig}
|
||||
thumbTintColor={isAndroid && COLOR_PRIMARY}
|
||||
minimumTrackTintColor={COLOR_PRIMARY}
|
||||
thumbTintColor={isAndroid && themes[theme].tintColor}
|
||||
minimumTrackTintColor={themes[theme].tintColor}
|
||||
maximumTrackTintColor={themes[theme].auxiliaryText}
|
||||
onValueChange={this.onValueChange}
|
||||
thumbImage={isIOS && { uri: 'audio_thumb', scale: Dimensions.get('window').scale }}
|
||||
/>
|
||||
<Text style={styles.duration}>{this.duration}</Text>
|
||||
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
|
||||
</View>
|
||||
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withSplit(Audio);
|
||||
|
|
|
@ -7,9 +7,10 @@ import { CustomIcon } from '../../lib/Icons';
|
|||
import styles from './styles';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import I18n from '../../i18n';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const Broadcast = React.memo(({
|
||||
author, user, broadcast, replyBroadcast
|
||||
author, user, broadcast, replyBroadcast, theme
|
||||
}) => {
|
||||
const isOwn = author._id === user.id;
|
||||
if (broadcast && !isOwn) {
|
||||
|
@ -17,25 +18,26 @@ const Broadcast = React.memo(({
|
|||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Reply')}</Text>
|
||||
</>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, () => true);
|
||||
});
|
||||
|
||||
Broadcast.propTypes = {
|
||||
author: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
broadcast: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
replyBroadcast: PropTypes.func
|
||||
};
|
||||
Broadcast.displayName = 'MessageBroadcast';
|
||||
|
|
|
@ -7,31 +7,33 @@ import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
|
|||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const CallButton = React.memo(({
|
||||
dlm, callJitsi
|
||||
dlm, theme, callJitsi
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={callJitsi}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<>
|
||||
<CustomIcon name='video' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Click_to_join')}</Text>
|
||||
<CustomIcon name='video' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
|
||||
</>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
CallButton.propTypes = {
|
||||
dlm: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
callJitsi: PropTypes.func
|
||||
};
|
||||
CallButton.displayName = 'CallButton';
|
||||
|
|
|
@ -6,16 +6,17 @@ import I18n from '../../i18n';
|
|||
import styles from './styles';
|
||||
import Markdown from '../markdown';
|
||||
import { getInfoMessage } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const Content = React.memo((props) => {
|
||||
if (props.isInfo) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...props })}</Text>;
|
||||
return <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{getInfoMessage({ ...props })}</Text>;
|
||||
}
|
||||
|
||||
let content = null;
|
||||
|
||||
if (props.tmid && !props.msg) {
|
||||
content = <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
content = <Text style={[styles.text, { color: themes[props.theme].titleText }]}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
} else {
|
||||
content = (
|
||||
<Markdown
|
||||
|
@ -31,6 +32,7 @@ const Content = React.memo((props) => {
|
|||
useMarkdown={props.useMarkdown && (!props.tmid || props.isThreadRoom)}
|
||||
navToRoomInfo={props.navToRoomInfo}
|
||||
tmid={props.tmid}
|
||||
theme={props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -40,7 +42,7 @@ const Content = React.memo((props) => {
|
|||
{content}
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.isTemp === nextProps.isTemp && prevProps.msg === nextProps.msg);
|
||||
}, (prevProps, nextProps) => prevProps.isTemp === nextProps.isTemp && prevProps.msg === nextProps.msg && prevProps.theme === nextProps.theme);
|
||||
|
||||
Content.propTypes = {
|
||||
isTemp: PropTypes.bool,
|
||||
|
@ -48,6 +50,7 @@ Content.propTypes = {
|
|||
tmid: PropTypes.string,
|
||||
isThreadRoom: PropTypes.bool,
|
||||
msg: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
isEdited: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
baseUrl: PropTypes.string,
|
||||
|
|
|
@ -8,29 +8,30 @@ import styles from './styles';
|
|||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { DISCUSSION } from './constants';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const Discussion = React.memo(({
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
msg, dcount, dlm, onDiscussionPress, theme
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={styles.text}>{msg}</Text>
|
||||
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={[styles.text, { color: themes[theme].titleText }]}>{msg}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
||||
<Text style={[styles.buttonText, { color: themes[theme].titleText }]}>{buttonText}</Text>
|
||||
</>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
@ -44,6 +45,9 @@ const Discussion = React.memo(({
|
|||
if (prevProps.dlm !== nextProps.dlm) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
|
@ -51,6 +55,7 @@ Discussion.propTypes = {
|
|||
msg: PropTypes.string,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
onDiscussionPress: PropTypes.func
|
||||
};
|
||||
Discussion.displayName = 'MessageDiscussion';
|
||||
|
|
|
@ -8,27 +8,32 @@ import Touchable from 'react-native-platform-touchable';
|
|||
import Markdown from '../markdown';
|
||||
import styles from './styles';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
import { withSplit } from '../../split';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const Button = React.memo(({ children, onPress }) => (
|
||||
const Button = React.memo(({
|
||||
children, onPress, split, theme
|
||||
}) => (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.imageContainer, split && sharedStyles.tabletContent]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
{children}
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
const Image = React.memo(({ img }) => (
|
||||
const Image = React.memo(({ img, theme }) => (
|
||||
<FastImage
|
||||
style={styles.image}
|
||||
style={[styles.image, { borderColor: themes[theme].borderColor }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
));
|
||||
|
||||
const ImageContainer = React.memo(({
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji, split, theme
|
||||
}) => {
|
||||
const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
if (!img) {
|
||||
|
@ -39,21 +44,21 @@ const ImageContainer = React.memo(({
|
|||
|
||||
if (file.description) {
|
||||
return (
|
||||
<Button onPress={onPress}>
|
||||
<Button split={split} theme={theme} onPress={onPress}>
|
||||
<View>
|
||||
<Image img={img} />
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
<Image img={img} theme={theme} />
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onPress={onPress}>
|
||||
<Image img={img} />
|
||||
<Button split={split} theme={theme} onPress={onPress}>
|
||||
<Image img={img} theme={theme} />
|
||||
</Button>
|
||||
);
|
||||
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file));
|
||||
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme);
|
||||
|
||||
ImageContainer.propTypes = {
|
||||
file: PropTypes.object,
|
||||
|
@ -61,19 +66,24 @@ ImageContainer.propTypes = {
|
|||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
theme: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImageContainer';
|
||||
|
||||
Image.propTypes = {
|
||||
img: PropTypes.string
|
||||
img: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImage';
|
||||
|
||||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onPress: PropTypes.func
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
ImageContainer.displayName = 'MessageButton';
|
||||
|
||||
export default ImageContainer;
|
||||
export default withSplit(ImageContainer);
|
||||
|
|
|
@ -5,7 +5,6 @@ import Touchable from 'react-native-platform-touchable';
|
|||
|
||||
import User from './User';
|
||||
import styles from './styles';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import RepliedThread from './RepliedThread';
|
||||
import MessageAvatar from './MessageAvatar';
|
||||
import Attachments from './Attachments';
|
||||
|
@ -52,11 +51,11 @@ MessageInner.displayName = 'MessageInner';
|
|||
|
||||
const Message = React.memo((props) => {
|
||||
if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
|
||||
const thread = props.isThreadReply ? <RepliedThread isTemp={props.isTemp} {...props} /> : null;
|
||||
const thread = props.isThreadReply ? <RepliedThread {...props} /> : null;
|
||||
return (
|
||||
<View style={[styles.container, props.style]}>
|
||||
{thread}
|
||||
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||
<View style={[styles.flex, styles.center]}>
|
||||
<MessageAvatar small {...props} />
|
||||
<View
|
||||
style={[
|
||||
|
@ -85,6 +84,7 @@ const Message = React.memo((props) => {
|
|||
<ReadReceipt
|
||||
isReadReceiptEnabled={props.isReadReceiptEnabled}
|
||||
unread={props.unread}
|
||||
theme={props.theme}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -134,7 +134,8 @@ Message.propTypes = {
|
|||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
isReadReceiptEnabled: PropTypes.bool,
|
||||
unread: PropTypes.bool
|
||||
unread: PropTypes.bool,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
MessageInner.propTypes = {
|
||||
|
|
|
@ -3,24 +3,25 @@ import Touchable from 'react-native-platform-touchable';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_DANGER } from '../../constants/colors';
|
||||
import styles from './styles';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const MessageError = React.memo(({ hasError, onErrorPress }) => {
|
||||
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
|
||||
if (!hasError) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
|
||||
<CustomIcon name='warning' color={COLOR_DANGER} size={18} />
|
||||
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
|
||||
</Touchable>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError);
|
||||
}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError && prevProps.theme === nextProps.theme);
|
||||
|
||||
MessageError.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
onErrorPress: PropTypes.func
|
||||
onErrorPress: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
MessageError.displayName = 'MessageError';
|
||||
|
||||
|
|
|
@ -7,24 +7,26 @@ import { CustomIcon } from '../../lib/Icons';
|
|||
import styles from './styles';
|
||||
import Emoji from './Emoji';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
|
||||
const AddReaction = React.memo(({ reactionInit }) => (
|
||||
const AddReaction = React.memo(({ reactionInit, theme }) => (
|
||||
<Touchable
|
||||
onPress={reactionInit}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={styles.reactionButton}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.reactionButton, { backgroundColor: themes[theme].backgroundColor }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={styles.reactionContainer}>
|
||||
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
|
||||
<View style={[styles.reactionContainer, { borderColor: themes[theme].borderColor }]}>
|
||||
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
|
||||
</View>
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
const Reaction = React.memo(({
|
||||
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji
|
||||
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme
|
||||
}) => {
|
||||
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
|
||||
return (
|
||||
|
@ -33,11 +35,11 @@ const Reaction = React.memo(({
|
|||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.reactionButton, { backgroundColor: reacted ? themes[theme].bannerBackground : themes[theme].backgroundColor }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<View style={[styles.reactionContainer, { borderColor: reacted ? themes[theme].tintColor : themes[theme].borderColor }]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
|
@ -45,14 +47,14 @@ const Reaction = React.memo(({
|
|||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
<Text style={[styles.reactionCount, { color: themes[theme].tintColor }]}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
|
||||
const Reactions = React.memo(({
|
||||
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji
|
||||
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme
|
||||
}) => {
|
||||
if (!reactions || reactions.length === 0) {
|
||||
return null;
|
||||
|
@ -68,9 +70,10 @@ const Reactions = React.memo(({
|
|||
onReactionLongPress={onReactionLongPress}
|
||||
onReactionPress={onReactionPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
<AddReaction reactionInit={reactionInit} />
|
||||
<AddReaction reactionInit={reactionInit} theme={theme} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
@ -81,7 +84,8 @@ Reaction.propTypes = {
|
|||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Reaction.displayName = 'MessageReaction';
|
||||
|
||||
|
@ -92,13 +96,15 @@ Reactions.propTypes = {
|
|||
onReactionPress: PropTypes.func,
|
||||
reactionInit: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Reactions.displayName = 'MessageReactions';
|
||||
|
||||
AddReaction.propTypes = {
|
||||
reactionInit: PropTypes.func
|
||||
reactionInit: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
AddReaction.displayName = 'MessageAddReaction';
|
||||
|
||||
export default Reactions;
|
||||
export default withTheme(Reactions);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COLOR_PRIMARY } from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
|
||||
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }) => {
|
||||
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread, theme }) => {
|
||||
if (isReadReceiptEnabled && !unread && unread !== null) {
|
||||
return <CustomIcon name='check' color={COLOR_PRIMARY} size={15} style={styles.readReceipt} />;
|
||||
return <CustomIcon name='check' color={themes[theme].tintColor} size={15} style={styles.readReceipt} />;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
@ -15,7 +15,8 @@ ReadReceipt.displayName = 'MessageReadReceipt';
|
|||
|
||||
ReadReceipt.propTypes = {
|
||||
isReadReceiptEnabled: PropTypes.bool,
|
||||
unread: PropTypes.bool
|
||||
unread: PropTypes.bool,
|
||||
theme: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ReadReceipt;
|
||||
|
|
|
@ -7,11 +7,12 @@ import PropTypes from 'prop-types';
|
|||
import { CustomIcon } from '../../lib/Icons';
|
||||
import DisclosureIndicator from '../DisclosureIndicator';
|
||||
import styles from './styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const RepliedThread = React.memo(({
|
||||
tmid, tmsg, isHeader, isTemp, fetchThreadName, id
|
||||
tmid, tmsg, isHeader, fetchThreadName, id, theme
|
||||
}) => {
|
||||
if (!tmid || !isHeader || isTemp) {
|
||||
if (!tmid || !isHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -25,9 +26,9 @@ const RepliedThread = React.memo(({
|
|||
|
||||
return (
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator />
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
|
||||
<Text style={[styles.repliedThreadName, { color: themes[theme].tintColor }]} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator theme={theme} />
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
|
@ -40,7 +41,7 @@ const RepliedThread = React.memo(({
|
|||
if (prevProps.isHeader !== nextProps.isHeader) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isTemp !== nextProps.isTemp) {
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -51,7 +52,7 @@ RepliedThread.propTypes = {
|
|||
tmsg: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
isHeader: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
fetchThreadName: PropTypes.func
|
||||
};
|
||||
RepliedThread.displayName = 'MessageRepliedThread';
|
||||
|
|
|
@ -8,7 +8,8 @@ import isEqual from 'deep-equal';
|
|||
import Markdown from '../markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withSplit } from '../../split';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
|
@ -16,9 +17,7 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
alignSelf: 'flex-end',
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
alignSelf: 'flex-start',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
},
|
||||
|
@ -36,13 +35,11 @@ const styles = StyleSheet.create({
|
|||
author: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
marginLeft: 10,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
},
|
||||
|
@ -57,12 +54,10 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
fieldTitle: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
fieldValue: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
marginTop: {
|
||||
|
@ -70,21 +65,21 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const Title = React.memo(({ attachment, timeFormat }) => {
|
||||
const Title = React.memo(({ attachment, timeFormat, theme }) => {
|
||||
if (!attachment.author_name) {
|
||||
return null;
|
||||
}
|
||||
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
|
||||
return (
|
||||
<View style={styles.authorContainer}>
|
||||
{attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null}
|
||||
{time ? <Text style={styles.time}>{ time }</Text> : null}
|
||||
{attachment.author_name ? <Text style={[styles.author, { color: themes[theme].titleText }]}>{attachment.author_name}</Text> : null}
|
||||
{time ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{ time }</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}, () => true);
|
||||
});
|
||||
|
||||
const Description = React.memo(({
|
||||
attachment, baseUrl, user, getCustomEmoji, useMarkdown
|
||||
attachment, baseUrl, user, getCustomEmoji, useMarkdown, theme
|
||||
}) => {
|
||||
const text = attachment.text || attachment.title;
|
||||
if (!text) {
|
||||
|
@ -97,6 +92,7 @@ const Description = React.memo(({
|
|||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useMarkdown={useMarkdown}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
|
@ -106,10 +102,13 @@ const Description = React.memo(({
|
|||
if (prevProps.attachment.title !== nextProps.attachment.title) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const Fields = React.memo(({ attachment }) => {
|
||||
const Fields = React.memo(({ attachment, theme }) => {
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
}
|
||||
|
@ -117,16 +116,16 @@ const Fields = React.memo(({ attachment }) => {
|
|||
<View style={styles.fieldsContainer}>
|
||||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={styles.fieldTitle}>{field.title}</Text>
|
||||
<Text style={styles.fieldValue}>{field.value}</Text>
|
||||
<Text style={[styles.fieldTitle, { color: themes[theme].titleText }]}>{field.title}</Text>
|
||||
<Text style={[styles.fieldValue, { color: themes[theme].titleText }]}>{field.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields));
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
|
||||
|
||||
const Reply = React.memo(({
|
||||
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown
|
||||
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown, split, theme
|
||||
}) => {
|
||||
if (!attachment) {
|
||||
return null;
|
||||
|
@ -140,17 +139,25 @@ const Reply = React.memo(({
|
|||
if (attachment.type === 'file') {
|
||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
openLink(url);
|
||||
openLink(url, theme);
|
||||
};
|
||||
|
||||
return (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[styles.button, index > 0 && styles.marginTop]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[
|
||||
styles.button,
|
||||
index > 0 && styles.marginTop,
|
||||
{
|
||||
backgroundColor: themes[theme].chatComponentBackground,
|
||||
borderColor: themes[theme].borderColor
|
||||
},
|
||||
split && sharedStyles.tabletContent
|
||||
]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<View style={styles.attachmentContainer}>
|
||||
<Title attachment={attachment} timeFormat={timeFormat} />
|
||||
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
|
||||
<Description
|
||||
attachment={attachment}
|
||||
timeFormat={timeFormat}
|
||||
|
@ -158,12 +165,13 @@ const Reply = React.memo(({
|
|||
user={user}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useMarkdown={useMarkdown}
|
||||
theme={theme}
|
||||
/>
|
||||
<Fields attachment={attachment} />
|
||||
<Fields attachment={attachment} theme={theme} />
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment));
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme);
|
||||
|
||||
Reply.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
|
@ -172,13 +180,16 @@ Reply.propTypes = {
|
|||
user: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
theme: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
Reply.displayName = 'MessageReply';
|
||||
|
||||
Title.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
timeFormat: PropTypes.string
|
||||
timeFormat: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Title.displayName = 'MessageReplyTitle';
|
||||
|
||||
|
@ -187,13 +198,15 @@ Description.propTypes = {
|
|||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Description.displayName = 'MessageReplyDescription';
|
||||
|
||||
Fields.propTypes = {
|
||||
attachment: PropTypes.object
|
||||
attachment: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Fields.displayName = 'MessageReplyFields';
|
||||
|
||||
export default Reply;
|
||||
export default withSplit(Reply);
|
||||
|
|
|
@ -6,9 +6,10 @@ import { formatLastMessage, formatMessageCount } from './utils';
|
|||
import styles from './styles';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { THREAD } from './constants';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const Thread = React.memo(({
|
||||
msg, tcount, tlm, customThreadTimeFormat, isThreadRoom
|
||||
msg, tcount, tlm, customThreadTimeFormat, isThreadRoom, theme
|
||||
}) => {
|
||||
if (!tlm || isThreadRoom || tcount === 0) {
|
||||
return null;
|
||||
|
@ -19,25 +20,29 @@ const Thread = React.memo(({
|
|||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<View
|
||||
style={[styles.button, styles.smallButton]}
|
||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
||||
testID={`message-thread-button-${ msg }`}
|
||||
>
|
||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
<CustomIcon name='thread' size={20} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} />
|
||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
|
||||
</View>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.tcount !== nextProps.tcount) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Thread.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
tcount: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
tlm: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
isThreadRoom: PropTypes.bool
|
||||
|
|
|
@ -7,9 +7,9 @@ import isEqual from 'lodash/isEqual';
|
|||
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY
|
||||
} from '../../constants/colors';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
import { withSplit } from '../../split';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
|
@ -19,8 +19,6 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1
|
||||
},
|
||||
textContainer: {
|
||||
|
@ -31,13 +29,11 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'flex-start'
|
||||
},
|
||||
title: {
|
||||
color: COLOR_PRIMARY,
|
||||
fontSize: 16,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
description: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
marginTop: {
|
||||
|
@ -59,10 +55,10 @@ const UrlImage = React.memo(({ image, user, baseUrl }) => {
|
|||
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
||||
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
|
||||
|
||||
const UrlContent = React.memo(({ title, description }) => (
|
||||
const UrlContent = React.memo(({ title, description, theme }) => (
|
||||
<View style={styles.textContainer}>
|
||||
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
|
||||
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
|
||||
{title ? <Text style={[styles.title, { color: themes[theme].tintColor }]} numberOfLines={2}>{title}</Text> : null}
|
||||
{description ? <Text style={[styles.description, { color: themes[theme].auxiliaryText }]} numberOfLines={2}>{description}</Text> : null}
|
||||
</View>
|
||||
), (prevProps, nextProps) => {
|
||||
if (prevProps.title !== nextProps.title) {
|
||||
|
@ -71,41 +67,55 @@ const UrlContent = React.memo(({ title, description }) => (
|
|||
if (prevProps.description !== nextProps.description) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const Url = React.memo(({
|
||||
url, index, user, baseUrl
|
||||
url, index, user, baseUrl, split, theme
|
||||
}) => {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPress = () => openLink(url.url);
|
||||
const onPress = () => openLink(url.url, theme);
|
||||
|
||||
return (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[styles.button, index > 0 && styles.marginTop, styles.container]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[
|
||||
styles.button,
|
||||
index > 0 && styles.marginTop,
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: themes[theme].chatComponentBackground,
|
||||
borderColor: themes[theme].borderColor
|
||||
},
|
||||
split && sharedStyles.tabletContent
|
||||
]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<>
|
||||
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
|
||||
<UrlContent title={url.title} description={url.description} />
|
||||
<UrlContent title={url.title} description={url.description} theme={theme} />
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
|
||||
|
||||
const Urls = React.memo(({ urls, user, baseUrl }) => {
|
||||
const Urls = React.memo(({
|
||||
urls, user, baseUrl, split, theme
|
||||
}) => {
|
||||
if (!urls || urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} />
|
||||
));
|
||||
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls));
|
||||
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
|
||||
|
||||
UrlImage.propTypes = {
|
||||
image: PropTypes.string,
|
||||
|
@ -116,7 +126,8 @@ UrlImage.displayName = 'MessageUrlImage';
|
|||
|
||||
UrlContent.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string
|
||||
description: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
UrlContent.displayName = 'MessageUrlContent';
|
||||
|
||||
|
@ -124,15 +135,19 @@ Url.propTypes = {
|
|||
url: PropTypes.object.isRequired,
|
||||
index: PropTypes.number,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
baseUrl: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
Url.displayName = 'MessageUrl';
|
||||
|
||||
Urls.propTypes = {
|
||||
urls: PropTypes.array,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
baseUrl: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
Urls.displayName = 'MessageUrls';
|
||||
|
||||
export default Urls;
|
||||
export default withTheme(withSplit(Urls));
|
||||
|
|
|
@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
|
|||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import moment from 'moment';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
|
||||
import MessageError from './MessageError';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import messageStyles from './styles';
|
||||
|
@ -16,7 +19,6 @@ const styles = StyleSheet.create({
|
|||
username: {
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
titleContainer: {
|
||||
|
@ -26,29 +28,28 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
alias: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
||||
const User = React.memo(({
|
||||
isHeader, useRealName, author, alias, ts, timeFormat, hasError, ...props
|
||||
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, ...props
|
||||
}) => {
|
||||
if (isHeader || hasError) {
|
||||
const username = (useRealName && author.name) || author.username;
|
||||
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
|
||||
const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null;
|
||||
const time = moment(ts).format(timeFormat);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.username} numberOfLines={1}>
|
||||
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||
{alias || username}
|
||||
{aliasUsername}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={messageStyles.time}>{time}</Text>
|
||||
{ hasError && <MessageError hasError={hasError} {...props} /> }
|
||||
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
{ hasError && <MessageError hasError={hasError} theme={theme} {...props} /> }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -62,8 +63,9 @@ User.propTypes = {
|
|||
author: PropTypes.object,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.instanceOf(Date),
|
||||
timeFormat: PropTypes.string
|
||||
timeFormat: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
User.displayName = 'MessageUser';
|
||||
|
||||
export default User;
|
||||
export default withTheme(User);
|
||||
|
|
|
@ -6,9 +6,11 @@ import isEqual from 'deep-equal';
|
|||
|
||||
import Markdown from '../markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
|
||||
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
|
||||
|
@ -18,22 +20,14 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
borderRadius: 4,
|
||||
height: 150,
|
||||
backgroundColor: '#1f2329',
|
||||
marginBottom: 6,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
modal: {
|
||||
margin: 0,
|
||||
backgroundColor: '#000'
|
||||
},
|
||||
image: {
|
||||
color: 'white'
|
||||
}
|
||||
});
|
||||
|
||||
const Video = React.memo(({
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji, theme
|
||||
}) => {
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
|
@ -44,26 +38,26 @@ const Video = React.memo(({
|
|||
return onOpenFileModal(file);
|
||||
}
|
||||
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
||||
openLink(uri);
|
||||
openLink(uri, theme);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={styles.button}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, { backgroundColor: themes[theme].videoBackground }, isTablet && sharedStyles.tabletContent]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<CustomIcon
|
||||
name='play'
|
||||
size={54}
|
||||
style={styles.image}
|
||||
color={themes[theme].buttonText}
|
||||
/>
|
||||
</Touchable>
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
|
||||
</>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme);
|
||||
|
||||
Video.propTypes = {
|
||||
file: PropTypes.object,
|
||||
|
@ -71,7 +65,8 @@ Video.propTypes = {
|
|||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default Video;
|
||||
|
|
|
@ -6,8 +6,9 @@ import Message from './Message';
|
|||
import debounce from '../../utils/debounce';
|
||||
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
import { withTheme } from '../../theme';
|
||||
|
||||
export default class MessageContainer extends React.Component {
|
||||
class MessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
user: PropTypes.shape({
|
||||
|
@ -42,13 +43,15 @@ export default class MessageContainer extends React.Component {
|
|||
onOpenFileModal: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
navToRoomInfo: PropTypes.func,
|
||||
callJitsi: PropTypes.func
|
||||
callJitsi: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onLongPress: () => {},
|
||||
archived: false,
|
||||
broadcast: false
|
||||
broadcast: false,
|
||||
theme: 'light'
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -61,7 +64,11 @@ export default class MessageContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -205,7 +212,7 @@ export default class MessageContainer extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi
|
||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, theme
|
||||
} = this.props;
|
||||
const {
|
||||
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
|
||||
|
@ -272,7 +279,10 @@ export default class MessageContainer extends React.Component {
|
|||
getCustomEmoji={getCustomEmoji}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
callJitsi={callJitsi}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(MessageContainer);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE
|
||||
} from '../../constants/colors';
|
||||
import { isTablet } from '../../utils/deviceInfo';
|
||||
|
||||
export default StyleSheet.create({
|
||||
root: {
|
||||
|
@ -25,6 +23,9 @@ export default StyleSheet.create({
|
|||
messageContentWithError: {
|
||||
marginLeft: 0
|
||||
},
|
||||
center: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
flex: {
|
||||
flexDirection: 'row'
|
||||
// flex: 1
|
||||
|
@ -43,27 +44,19 @@ export default StyleSheet.create({
|
|||
marginBottom: 6,
|
||||
borderRadius: 2
|
||||
},
|
||||
reactionButtonReacted: {
|
||||
backgroundColor: '#e8f2ff'
|
||||
},
|
||||
reactionContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 2,
|
||||
borderWidth: 1,
|
||||
borderColor: COLOR_BORDER,
|
||||
height: 28,
|
||||
minWidth: 46.3
|
||||
},
|
||||
reactedContainer: {
|
||||
borderColor: COLOR_PRIMARY
|
||||
},
|
||||
reactionCount: {
|
||||
fontSize: 14,
|
||||
marginLeft: 3,
|
||||
marginRight: 8.5,
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactionEmoji: {
|
||||
|
@ -81,9 +74,6 @@ export default StyleSheet.create({
|
|||
avatarSmall: {
|
||||
marginLeft: 16
|
||||
},
|
||||
addReaction: {
|
||||
color: COLOR_PRIMARY
|
||||
},
|
||||
errorButton: {
|
||||
paddingLeft: 10,
|
||||
paddingVertical: 5
|
||||
|
@ -99,18 +89,15 @@ export default StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: COLOR_PRIMARY,
|
||||
borderRadius: 2
|
||||
},
|
||||
smallButton: {
|
||||
height: 30
|
||||
},
|
||||
buttonIcon: {
|
||||
color: COLOR_WHITE,
|
||||
marginRight: 6
|
||||
},
|
||||
buttonText: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
|
@ -121,10 +108,9 @@ export default StyleSheet.create({
|
|||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
minHeight: 200,
|
||||
// maxWidth: 400,
|
||||
minHeight: isTablet ? 300 : 200,
|
||||
borderRadius: 4,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1
|
||||
},
|
||||
imagePressed: {
|
||||
|
@ -137,27 +123,23 @@ export default StyleSheet.create({
|
|||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
textInfo: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
startedDiscussion: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: 16,
|
||||
marginBottom: 6,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
paddingLeft: 10,
|
||||
lineHeight: 22,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
},
|
||||
|
@ -169,14 +151,12 @@ export default StyleSheet.create({
|
|||
marginBottom: 12
|
||||
},
|
||||
repliedThreadIcon: {
|
||||
color: COLOR_PRIMARY,
|
||||
marginRight: 10,
|
||||
marginLeft: 16
|
||||
},
|
||||
repliedThreadName: {
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
readReceipt: {
|
||||
|
|
|
@ -95,6 +95,7 @@ export default {
|
|||
announcement: 'announcement',
|
||||
Announcement: 'Announcement',
|
||||
Apply_Your_Certificate: 'Apply Your Certificate',
|
||||
Applying_a_theme_will_change_how_the_app_looks: 'Applying a theme will change how the app looks.',
|
||||
ARCHIVE: 'ARCHIVE',
|
||||
archive: 'archive',
|
||||
are_typing: 'are typing',
|
||||
|
@ -102,11 +103,13 @@ export default {
|
|||
Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?',
|
||||
Audio: 'Audio',
|
||||
Authenticating: 'Authenticating',
|
||||
Automatic: 'Automatic',
|
||||
Auto_Translate: 'Auto-Translate',
|
||||
Avatar_changed_successfully: 'Avatar changed successfully!',
|
||||
Avatar_Url: 'Avatar URL',
|
||||
Away: 'Away',
|
||||
Back: 'Back',
|
||||
Black: 'Black',
|
||||
Block_user: 'Block user',
|
||||
Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply',
|
||||
Broadcast_Channel: 'Broadcast Channel',
|
||||
|
@ -136,6 +139,7 @@ export default {
|
|||
connecting_server: 'connecting to server',
|
||||
Connecting: 'Connecting...',
|
||||
Contact_us: 'Contact us',
|
||||
Contact_your_server_admin: 'Contact your server admin.',
|
||||
Continue_with: 'Continue with',
|
||||
Copied_to_clipboard: 'Copied to clipboard!',
|
||||
Copy: 'Copy',
|
||||
|
@ -147,6 +151,8 @@ export default {
|
|||
Created_snippet: 'Created a snippet',
|
||||
Create_a_new_workspace: 'Create a new workspace',
|
||||
Create: 'Create',
|
||||
Dark: 'Dark',
|
||||
Dark_level: 'Dark Level',
|
||||
Default: 'Default',
|
||||
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
|
||||
delete: 'delete',
|
||||
|
@ -211,6 +217,7 @@ export default {
|
|||
leaving_room: 'leaving room',
|
||||
leave: 'leave',
|
||||
Legal: 'Legal',
|
||||
Light: 'Light',
|
||||
License: 'License',
|
||||
Livechat: 'Livechat',
|
||||
Login: 'Login',
|
||||
|
@ -257,6 +264,7 @@ export default {
|
|||
No_Reactions: 'No Reactions',
|
||||
No_Read_Receipts: 'No Read Receipts',
|
||||
Not_logged: 'Not logged',
|
||||
Not_RC_Server: 'This is not a Rocket.Chat server.\n{{contact}}',
|
||||
Nothing: 'Nothing',
|
||||
Nothing_to_save: 'Nothing to save!',
|
||||
Notify_active_in_this_room: 'Notify active users in this room',
|
||||
|
@ -277,6 +285,7 @@ export default {
|
|||
pinned: 'pinned',
|
||||
Pinned: 'Pinned',
|
||||
Please_enter_your_password: 'Please enter your password',
|
||||
Preferences: 'Preferences',
|
||||
Preferences_saved: 'Preferences saved!',
|
||||
Privacy_Policy: ' Privacy Policy',
|
||||
Private_Channel: 'Private Channel',
|
||||
|
@ -375,7 +384,7 @@ export default {
|
|||
Tap_to_view_servers_list: 'Tap to view servers list',
|
||||
Terms_of_Service: ' Terms of Service ',
|
||||
Theme: 'Theme',
|
||||
The_URL_is_invalid: 'The URL you entered is invalid. Check it and try again, please!',
|
||||
The_URL_is_invalid: 'Invalid URL or unable to establish a secure connection.\n{{contact}}',
|
||||
There_was_an_error_while_action: 'There was an error while {{action}}!',
|
||||
This_room_is_blocked: 'This room is blocked',
|
||||
This_room_is_read_only: 'This room is read only',
|
||||
|
@ -420,6 +429,7 @@ export default {
|
|||
Video_call: 'Video call',
|
||||
View_Original: 'View Original',
|
||||
Voice_call: 'Voice call',
|
||||
Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}',
|
||||
Welcome: 'Welcome',
|
||||
Welcome_to_RocketChat: 'Welcome to Rocket.Chat',
|
||||
Whats_your_2fa: 'What\'s your 2FA code?',
|
||||
|
@ -438,5 +448,19 @@ export default {
|
|||
Version_no: 'Version: {{version}}',
|
||||
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
||||
Change_Language: 'Change Language',
|
||||
Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.'
|
||||
Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.',
|
||||
Type_message: 'Type message',
|
||||
Room_search: 'Rooms search',
|
||||
Room_selection: 'Room selection 1...9',
|
||||
Next_room: 'Next room',
|
||||
Previous_room: 'Previous room',
|
||||
New_room: 'New room',
|
||||
Upload_room: 'Upload to room',
|
||||
Search_messages: 'Search messages',
|
||||
Scroll_messages: 'Scroll messages',
|
||||
Reply_latest: 'Reply to latest',
|
||||
Server_selection: 'Server selection',
|
||||
Server_selection_numbers: 'Server selection 1...9',
|
||||
Add_server: 'Add server',
|
||||
New_line: 'New line'
|
||||
};
|
||||
|
|
|
@ -99,16 +99,19 @@ export default {
|
|||
and: 'e',
|
||||
announcement: 'anúncio',
|
||||
Announcement: 'Anúncio',
|
||||
Applying_a_theme_will_change_how_the_app_looks: 'Aplicar um tema mudará a aparência do app.',
|
||||
ARCHIVE: 'ARQUIVAR',
|
||||
archive: 'arquivar',
|
||||
are_typing: 'estão digitando',
|
||||
Are_you_sure_question_mark: 'Você tem certeza?',
|
||||
Are_you_sure_you_want_to_leave_the_room: 'Tem certeza de que deseja sair da sala {{room}}?',
|
||||
Authenticating: 'Autenticando',
|
||||
Automatic: 'Automático',
|
||||
Avatar_changed_successfully: 'Avatar alterado com sucesso!',
|
||||
Avatar_Url: 'Avatar URL',
|
||||
Away: 'Ausente',
|
||||
Back: 'Voltar',
|
||||
Black: 'Preto',
|
||||
Block_user: 'Bloquear usuário',
|
||||
Broadcast_channel_Description: 'Somente usuários autorizados podem escrever novas mensagens, mas os outros usuários poderão responder',
|
||||
Broadcast_Channel: 'Canal de Transmissão',
|
||||
|
@ -138,6 +141,7 @@ export default {
|
|||
connecting_server: 'conectando no servidor',
|
||||
Connecting: 'Conectando...',
|
||||
Continue_with: 'Entrar com',
|
||||
Contact_your_server_admin: 'Contate o administrador do servidor.',
|
||||
Copied_to_clipboard: 'Copiado para a área de transferência!',
|
||||
Copy: 'Copiar',
|
||||
Permalink: 'Link-Permanente',
|
||||
|
@ -146,6 +150,8 @@ export default {
|
|||
Created_snippet: 'Criou um snippet',
|
||||
Create_a_new_workspace: 'Criar nova área de trabalho',
|
||||
Create: 'Criar',
|
||||
Dark: 'Escuro',
|
||||
Dark_level: 'Nível escuro',
|
||||
Delete_Room_Warning: 'A exclusão de uma sala irá apagar todas as mensagens postadas na sala. Isso não pode ser desfeito.',
|
||||
delete: 'excluir',
|
||||
Delete: 'Excluir',
|
||||
|
@ -199,6 +205,7 @@ export default {
|
|||
leaving_room: 'saindo do canal',
|
||||
leave: 'sair',
|
||||
Legal: 'Legal',
|
||||
Light: 'Claro',
|
||||
Livechat: 'Livechat',
|
||||
Login: 'Entrar',
|
||||
Login_error: 'Suas credenciais foram rejeitadas. Tente novamente por favor!',
|
||||
|
@ -242,6 +249,7 @@ export default {
|
|||
Nothing_to_save: 'Nada para salvar!',
|
||||
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
|
||||
Notify_all_in_this_room: 'Notificar todos nesta sala',
|
||||
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}',
|
||||
Offline: 'Offline',
|
||||
Oops: 'Ops!',
|
||||
Online: 'Online',
|
||||
|
@ -255,6 +263,7 @@ export default {
|
|||
pinned: 'fixada',
|
||||
Pinned: 'Mensagens Fixadas',
|
||||
Please_enter_your_password: 'Por favor, digite sua senha',
|
||||
Preferences: 'Preferências',
|
||||
Preferences_saved: 'Preferências salvas!',
|
||||
Privacy_Policy: ' Política de Privacidade',
|
||||
Private_Channel: 'Canal Privado',
|
||||
|
@ -334,7 +343,8 @@ export default {
|
|||
Take_a_photo: 'Tirar uma foto',
|
||||
Take_a_video: 'Gravar um vídeo',
|
||||
Terms_of_Service: ' Termos de Serviço ',
|
||||
The_URL_is_invalid: 'A URL fornecida é inválida ou não acessível. Por favor tente novamente, mas com uma url diferente.',
|
||||
Theme: 'Tema',
|
||||
The_URL_is_invalid: 'A URL fornecida é inválida ou incapaz de estabelecer uma conexão segura.\n{{contact}}',
|
||||
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
|
||||
This_room_is_blocked: 'Este quarto está bloqueado',
|
||||
This_room_is_read_only: 'Este quarto é apenas de leitura',
|
||||
|
@ -375,6 +385,7 @@ export default {
|
|||
Username_or_email: 'Usuário ou email',
|
||||
Video_call: 'Chamada de vídeo',
|
||||
Voice_call: 'Chamada de voz',
|
||||
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',
|
||||
Welcome: 'Bem vindo',
|
||||
Welcome_to_RocketChat: 'Bem vindo ao Rocket.Chat',
|
||||
Whats_your_2fa: 'Qual seu código de autenticação?',
|
||||
|
@ -389,5 +400,20 @@ export default {
|
|||
you: 'você',
|
||||
You: 'Você',
|
||||
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Você precisa acessar ao menos um servidor Rocket.Chat para compartilhar.',
|
||||
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!'
|
||||
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
|
||||
Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.',
|
||||
Type_message: 'Digitar mensagem',
|
||||
Room_search: 'Busca de sala',
|
||||
Room_selection: 'Selecionar sala 1...9',
|
||||
Next_room: 'Próxima sala',
|
||||
Previous_room: 'Sala anterior',
|
||||
New_room: 'Nova sala',
|
||||
Upload_room: 'Enviar arquivo',
|
||||
Search_messages: 'Buscar mensagens',
|
||||
Scroll_messages: 'Rolar mensagens',
|
||||
Reply_latest: 'Responder para última mensagem',
|
||||
Server_selection: 'Seleção de servidor',
|
||||
Server_selection_numbers: 'Selecionar servidor 1...9',
|
||||
Add_server: 'Adicionar servidor',
|
||||
New_line: 'Nova linha'
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default {
|
||||
'1_person_reacted': '1 человек отреагировал',
|
||||
'1_user': '1 пользователь',
|
||||
'error-action-not-allowed': '{{action}} не допускается',
|
||||
'error-application-not-found': 'Приложение не найдено',
|
||||
'error-archived-duplicate-name': 'Есть архивный канал с именем {{room_name}}',
|
||||
|
@ -21,7 +22,7 @@ export default {
|
|||
'error-input-is-not-a-valid-field': '{{input}} недействительно {{field}}',
|
||||
'error-invalid-actionlink': 'Недействительная ссылка действия',
|
||||
'error-invalid-arguments': 'Недопустимые аргументы',
|
||||
'error-invalid-asset': 'Недопустимый актив',
|
||||
'error-invalid-asset': 'Недопустимый ресурс',
|
||||
'error-invalid-channel': 'Недействительный канал.',
|
||||
'error-invalid-channel-start-with-chars': 'Недействительный канал. Начните с @ или #',
|
||||
'error-invalid-custom-field': 'Неверное настраиваемое поле',
|
||||
|
@ -65,7 +66,7 @@ export default {
|
|||
'error-role-in-use': 'Невозможно удалить роль, потому что она используется',
|
||||
'error-role-name-required': 'Требуется имя роли',
|
||||
'error-the-field-is-required': 'Требуется поле {{field}}.',
|
||||
'error-too-many-requests': 'Ошибка, слишком много запросов. Пожалуйста, помедленнее. Вы должны подождать {{seconds}} секунд, прежде чем повторять попытку.',
|
||||
'error-too-many-requests': 'Ошибка, слишком много запросов. Пожалуйста, помедленнее. Вы должны подождать {{seconds}} секунд, прежде чем повторить попытку.',
|
||||
'error-user-is-not-activated': 'Пользователь не активирован',
|
||||
'error-user-has-no-roles': 'Пользователь не имеет ролей',
|
||||
'error-user-limit-exceeded': 'Количество пользователей, которых вы пытаетесь пригласить на #channel_name, превышает лимит, установленный администратором',
|
||||
|
@ -75,28 +76,37 @@ export default {
|
|||
'error-user-registration-secret': 'Регистрация пользователей разрешена только через секретный URL',
|
||||
'error-you-are-last-owner': 'Вы последний владелец. Пожалуйста, установите нового владельца, прежде чем покинуть комнату.',
|
||||
Actions: 'Действия',
|
||||
activity: 'активность',
|
||||
Activity: 'Активность',
|
||||
Add_Reaction: 'Добавить реакцию',
|
||||
Add_Server: 'Добавить сервер',
|
||||
Add_user: 'Добавить пользователя',
|
||||
Admin_Panel: 'Панель админа',
|
||||
Alert: 'Оповещение',
|
||||
alert: 'оповещение',
|
||||
alerts: 'оповещения',
|
||||
All_users_in_the_channel_can_write_new_messages: 'Все пользователи канала могут писать новые сообщения',
|
||||
All: 'Все',
|
||||
All_Messages: 'Все сообщения',
|
||||
Allow_Reactions: 'Разрешить реакции',
|
||||
Alphabetical: 'По алфавиту',
|
||||
and_more: 'и более',
|
||||
and: 'и',
|
||||
announcement: 'объявление',
|
||||
Announcement: 'Объявление',
|
||||
Apply_Your_Certificate: 'Применить ваш сертификат',
|
||||
ARCHIVE: 'АРХИВ',
|
||||
archive: 'архив',
|
||||
are_typing: 'печатают',
|
||||
Are_you_sure_question_mark: 'Вы уверены?',
|
||||
Are_you_sure_you_want_to_leave_the_room: 'Вы действительно хотите покинуть канал {{room}}?',
|
||||
Audio: 'Аудио',
|
||||
Authenticating: 'Аутентификация',
|
||||
Auto_Translate: 'Автоперевод',
|
||||
Avatar_changed_successfully: 'Аватар успешно изменен!',
|
||||
Avatar_Url: 'URL аватара',
|
||||
Away: 'Отошел',
|
||||
Back: 'Назад',
|
||||
Block_user: 'Блокировать пользователя',
|
||||
Broadcast_channel_Description: 'Только авторизованные пользователи могут писать новые сообщения, но другие пользователи смогут ответить',
|
||||
Broadcast_Channel: 'Широковещательный канал',
|
||||
|
@ -106,60 +116,109 @@ export default {
|
|||
Cancel_recording: 'Отменить запись',
|
||||
Cancel: 'Отмена',
|
||||
changing_avatar: 'изменение аватара',
|
||||
creating_channel: 'создание канала',
|
||||
Channel_Name: 'Название канала',
|
||||
Channels: 'Каналы',
|
||||
Chats: 'Чаты',
|
||||
Call_already_ended: 'Вызов уже завершен!',
|
||||
Click_to_join: 'Нажмите, чтобы присоединиться!',
|
||||
Close: 'Закрыть',
|
||||
Close_emoji_selector: 'Закрыть селектор emoji',
|
||||
Choose: 'Выбрать',
|
||||
Choose_from_library: 'Выбрать из библиотеки',
|
||||
Choose_file: 'Выбрать файл',
|
||||
Code: 'Код',
|
||||
Collaborative: 'Совместный',
|
||||
Confirm: 'Подтверждение',
|
||||
Connect: 'Соединение',
|
||||
Connected_to: 'Подключен к',
|
||||
Connecting: 'Соединение',
|
||||
Connect_to_a_server: 'Подключиться к серверу',
|
||||
Connected: 'Подключено',
|
||||
connecting_server: 'подключение к серверу',
|
||||
Connecting: 'Соединение...',
|
||||
Contact_us: 'Связаться с нами',
|
||||
Contact_your_server_admin: 'Свяжитесь с администратором сервера.',
|
||||
Continue_with: 'Продолжить с',
|
||||
Copied_to_clipboard: 'Скопировано в буфер обмена!',
|
||||
Copy: 'Копировать',
|
||||
Permalink: 'постоянную ссылку',
|
||||
Permalink: 'Постоянная ссылка',
|
||||
Certificate_password: 'Пароль сертификата',
|
||||
Whats_the_password_for_your_certificate: 'Какой пароль для вашего сертификата?',
|
||||
Create_account: 'Создать аккаунт',
|
||||
Create_Channel: 'Создать канал',
|
||||
Created_snippet: 'Создать сниппет',
|
||||
Create_a_new_workspace: 'Новое рабочее пространство',
|
||||
Create: 'Создать',
|
||||
Default: 'По умолчанию',
|
||||
Delete_Room_Warning: 'Удаление канала приведет к удалению всех сообщений, размещенных в нем. Это не может быть отменено.',
|
||||
delete: 'удалить',
|
||||
Delete: 'Удалить',
|
||||
DELETE: 'УДАЛИТЬ',
|
||||
description: 'описание',
|
||||
Description: 'Описание',
|
||||
DESKTOP_OPTIONS: 'ПАРАМЕТРЫ РАБОЧЕГО СТОЛА',
|
||||
Directory: 'Директория',
|
||||
Direct_Messages: 'Личные сообщения',
|
||||
Disable_notifications: 'Отключить уведомления',
|
||||
Discussions: 'Дискуссии',
|
||||
Dont_Have_An_Account: 'Нет аккаунта?',
|
||||
Do_you_have_a_certificate: 'У вас есть сертификат?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Вы действительно хотите {{key}} этот канал?',
|
||||
edit: 'редактировать',
|
||||
edited: 'отредактировано',
|
||||
Edit: 'Редактировать',
|
||||
Email_or_password_field_is_empty: 'Поле электронной почты или пароля пусты',
|
||||
Email: 'Электронная почта',
|
||||
Email: 'Email',
|
||||
EMAIL: 'EMAIL',
|
||||
email: 'e-mail',
|
||||
Enable_Auto_Translate: 'Включить автоперевод',
|
||||
Enable_markdown: 'Включить markdown',
|
||||
Enable_notifications: 'Включить уведомления',
|
||||
Everyone_can_access_this_channel: 'Каждый может получить доступ к этому каналу',
|
||||
erasing_room: 'стирание комнаты',
|
||||
Error_uploading: 'Ошибка при загрузке',
|
||||
Favorite: 'Избранное',
|
||||
Favorites: 'Избранные',
|
||||
Files: 'Файлы',
|
||||
File_description: 'Описание файла',
|
||||
File_name: 'Имя файла',
|
||||
Finish_recording: 'Завершить запись',
|
||||
Following_thread: 'Следить за темой',
|
||||
For_your_security_you_must_enter_your_current_password_to_continue: 'В целях вашей безопасности вы должны ввести свой текущий пароль для продолжения',
|
||||
Forgot_my_password: 'Забыл свой пароль',
|
||||
Forgot_password_If_this_email_is_registered: 'Если эта электронная почта зарегистрирована, мы отправим инструкции о том, как сбросить пароль. Если вы не получите письмо в ближайшее время, вернитесь и повторите попытку.',
|
||||
Forgot_password: 'Забыли пароль',
|
||||
Forgot_Password: 'Забыли Пароль',
|
||||
Full_table: 'Нажмите, чтобы увидеть полную таблицу',
|
||||
Group_by_favorites: 'По избранным',
|
||||
Group_by_type: 'По типу',
|
||||
Hide: 'Скрыть',
|
||||
Has_joined_the_channel: 'Присоединился к каналу',
|
||||
Has_joined_the_conversation: 'Присоединился к беседе',
|
||||
Has_left_the_channel: 'Покинул канал',
|
||||
I_have_an_account: 'У меня есть аккаунт',
|
||||
IN_APP_AND_DESKTOP: 'В приложении и на десктопе',
|
||||
In_App_and_Desktop_Alert_info: 'Отображает баннер в верхней части экрана, когда приложение открыто, и отображает уведомление на рабочем столе.',
|
||||
Invisible: 'Невидимый',
|
||||
Invite: 'Приглашение',
|
||||
is_a_valid_RocketChat_instance: 'является действительным сервером Rocket.Chat',
|
||||
is_not_a_valid_RocketChat_instance: 'не является действительным сервером Rocket.Chat',
|
||||
is_typing: 'печатает',
|
||||
Invalid_server_version: 'Сервер, к которому вы пытаетесь подключиться, использует версию, которая больше не поддерживается приложением: {{currentVersion}}.\n\nНам нужна версия {{minVersion}}',
|
||||
Join_the_community: 'Присоединиться к сообществу',
|
||||
Join: 'Присоединиться',
|
||||
Just_invited_people_can_access_this_channel: 'Только приглашенные люди могут получить доступ к этому каналу',
|
||||
Language: 'Язык',
|
||||
last_message: 'последнее сообщение',
|
||||
Leave_channel: 'Покинуть канал',
|
||||
leaving_room: 'покинуть комнату',
|
||||
leave: 'покинуть',
|
||||
Loading_messages_ellipsis: 'Загрузка сообщений ...',
|
||||
Legal: 'Правовые аспекты',
|
||||
License: 'Лицензия',
|
||||
Livechat: 'Livechat',
|
||||
Login: 'Вход',
|
||||
Login_error: 'Ваши учетные данные были отклонены! Пожалуйста, попробуйте еще раз.',
|
||||
Login_with: 'Войти с',
|
||||
Logout: 'Выйти',
|
||||
members: 'пользователи',
|
||||
Members: 'Пользователи',
|
||||
Mentioned_Messages: 'Упомянутые сообщения',
|
||||
mentioned: 'упомянутые',
|
||||
|
@ -168,35 +227,51 @@ export default {
|
|||
Message_actions: 'Действия с сообщением',
|
||||
Message_pinned: 'Сообщение прикреплено',
|
||||
Message_removed: 'Сообщение удалено',
|
||||
message: 'сообщение',
|
||||
messages: 'сообщения',
|
||||
Messages: 'Сообщения',
|
||||
Microphone_Permission_Message: 'Rocket Chat нуждается в доступе к вашему микрофону, чтобы вы могли отправлять аудиосообщения.',
|
||||
Message_Reported: 'Сообщение отправлено',
|
||||
Microphone_Permission_Message: 'Rocket Chat нужен доступ к вашему микрофону, чтобы вы могли отправлять аудиосообщения.',
|
||||
Microphone_Permission: 'Разрешение на использование микрофона',
|
||||
Mute: 'Заглушить',
|
||||
muted: 'Заглушен',
|
||||
My_servers: 'Мои серверы',
|
||||
N_person_reacted: '{{n}} людей отреагировало',
|
||||
N_users: '{{n}} пользователи',
|
||||
name: 'имя',
|
||||
Name: 'Имя',
|
||||
New_in_RocketChat_question_mark: 'Новичок в Rocket.Chat?',
|
||||
New_Message: 'Новое Сообщение',
|
||||
New_Password: 'Новый Пароль',
|
||||
New_Server: 'Новый Сервер',
|
||||
New_Message: 'Новое сообщение',
|
||||
New_Password: 'Новый пароль',
|
||||
New_Server: 'Новый сервер',
|
||||
No_files: 'Нет файлов',
|
||||
Next: 'Далее',
|
||||
No_mentioned_messages: 'Нет упоминаний',
|
||||
No_pinned_messages: 'Нет прикрепленных сообщений',
|
||||
No_results_found: 'Ничего не найдено',
|
||||
No_starred_messages: 'Нет отмеченных сообщений',
|
||||
No_thread_messages: 'Нет сообщений в теме',
|
||||
No_announcement_provided: 'Нет объявлений.',
|
||||
No_description_provided: 'Нет описания.',
|
||||
No_topic_provided: 'Нет темы.',
|
||||
No_Message: 'Нет сообщения',
|
||||
No_messages_yet: 'Пока нет сообщений',
|
||||
No_Reactions: 'Нет реакций',
|
||||
No_Read_Receipts: 'Нет информации о прочтении',
|
||||
Not_logged: 'Не зарегистрирован',
|
||||
Not_RC_Server: 'Это не сервер Rocket.Chat.\n{{contact}}',
|
||||
Nothing: 'Ничего',
|
||||
Nothing_to_save: 'Нечего сохранять!',
|
||||
Notify_active_in_this_room: 'Уведомить всех активных пользователей в этом чате',
|
||||
Notify_all_in_this_room: 'Уведомить всех в этом чате',
|
||||
Notifications: 'Уведомления',
|
||||
Notification_Duration: 'Продолжительность уведомлений',
|
||||
Notification_Preferences: 'Настройки уведомлений',
|
||||
Offline: 'Офлайн',
|
||||
Oops: 'Упс!',
|
||||
Online: 'Онлайн',
|
||||
Only_authorized_users_can_write_new_messages: 'Только авторизованные пользователи могут писать новые сообщения',
|
||||
Open_emoji_selector: 'Открыть селектор emoji',
|
||||
Open_Source_Communication: 'Общение с открытым кодом',
|
||||
Password: 'Пароль',
|
||||
Permalink_copied_to_clipboard: 'Постоянная ссылка скопирована в буфер обмена!',
|
||||
Pin: 'Прикрепить сообщение',
|
||||
|
@ -205,25 +280,40 @@ export default {
|
|||
Pinned: 'Прикреплено',
|
||||
Please_enter_your_password: 'Пожалуйста введите ваш пароль',
|
||||
Preferences_saved: 'Настройки сохранены!',
|
||||
Privacy_Policy: ' Политика Конфиденциальности',
|
||||
Privacy_Policy: ' Политика конфиденциальности',
|
||||
Private_Channel: 'Приватный канал',
|
||||
Private_Groups: 'Приватные группы',
|
||||
Private: 'Приватный',
|
||||
Processing: 'Обработка...',
|
||||
Profile_saved_successfully: 'Профиль успешно сохранен!',
|
||||
Profile: 'Профиль',
|
||||
Public_Channel: 'Публичный канал',
|
||||
Public: 'Публичный',
|
||||
PUSH_NOTIFICATIONS: 'PUSH УВЕДОМЛЕНИЯ',
|
||||
Push_Notifications_Alert_Info: 'Эти уведомления доставляются вам, когда приложение не открыто',
|
||||
Quote: 'Цитата',
|
||||
Reactions_are_disabled: 'Реакции отключены',
|
||||
Reactions_are_enabled: 'Реакции активированы',
|
||||
Reactions: 'Реакции',
|
||||
Read: 'Читать',
|
||||
Read_Only_Channel: 'Канал только для чтения',
|
||||
Read_Only: 'Только для чтения',
|
||||
Read_Receipt: 'Уведомление о прочтении',
|
||||
Receive_Group_Mentions: 'Получать групповые уведомления',
|
||||
Receive_Group_Mentions_Info: 'Получать @all и @here уведомления',
|
||||
Register: 'Зарегистрировать',
|
||||
Repeat_Password: 'Повторите пароль',
|
||||
Replied_on: 'Ответил на:',
|
||||
replies: 'ответы',
|
||||
reply: 'ответить',
|
||||
Reply: 'Ответить',
|
||||
Report: 'Жалоба',
|
||||
Receive_Notification: 'Получать уведомления',
|
||||
Receive_notifications_from: 'Получать уведомления от {{name}}',
|
||||
Resend: 'Отправить повторно',
|
||||
Reset_password: 'Сброс пароля',
|
||||
RESET: 'СБРОС',
|
||||
resetting_password: 'сброс пароля',
|
||||
Roles: 'Роли',
|
||||
Room_actions: 'Действия с каналом',
|
||||
Room_changed_announcement: 'Объявление канала было изменено на: {{объявление}} пользователем {{userBy}}',
|
||||
|
@ -243,59 +333,100 @@ export default {
|
|||
saving_settings: 'сохранение настроек',
|
||||
Search_Messages: 'Поиск сообщений',
|
||||
Search: 'Поиск',
|
||||
Search_by: 'Поиск по',
|
||||
Search_global_users: 'Поиск глобальных пользователей',
|
||||
Search_global_users_description: 'При активации станет возможен поиск пользователей на других серверах.',
|
||||
Seconds: '{{second}} секунд',
|
||||
Select_Avatar: 'Выбор аватара',
|
||||
Select_Server: 'Выбор сервера',
|
||||
Select_Users: 'Выбор пользователей',
|
||||
Send: 'Отправить',
|
||||
Send_audio_message: 'Отправить аудиосообщение',
|
||||
Send_crash_report: 'Отправить отчет об ошибке',
|
||||
Send_message: 'Отправить сообщение',
|
||||
Send_to: 'Отправить...',
|
||||
Sent_an_attachment: 'Отправить вложение',
|
||||
Server: 'Сервер',
|
||||
Servers: 'Серверы',
|
||||
Server_version: 'Версия сервера: {{version}}',
|
||||
Set_username_subtitle: 'Имя пользователя необходимо для того, чтобы позволить другим упомянуть вас в сообщениях',
|
||||
Settings: 'Настройки',
|
||||
Settings_succesfully_changed: 'Настройки успешно изменены!',
|
||||
Share: 'Поделиться',
|
||||
Share_this_app: 'Рассказать о приложении',
|
||||
Show_Unread_Counter: 'Показать счетчик непрочитанных',
|
||||
Show_Unread_Counter_Info: 'Счетчик непрочитанных отображается в виде значка справа от канала в списке каналов',
|
||||
Sign_in_your_server: 'Войдите на ваш сервер',
|
||||
Sign_Up: 'Регистрация',
|
||||
Some_field_is_invalid_or_empty: 'Некоторые поля недопустимы или пусты',
|
||||
Star_room: 'Star room',
|
||||
Star: 'Звезда',
|
||||
Starred_Messages: 'Помеченные сообщения',
|
||||
Sorting_by: 'Сортировать по {{key}}',
|
||||
Sound: 'Звук',
|
||||
Star_room: 'В избранное',
|
||||
Star: 'Отметить',
|
||||
Starred_Messages: 'Отмеченные сообщения',
|
||||
starred: 'отмечено',
|
||||
Starred: 'Отмечено',
|
||||
Start_of_conversation: 'Начало разговора',
|
||||
Started_discussion: 'Началось обсуждение :',
|
||||
Started_call: 'Звонок, начатый {{userBy}}',
|
||||
Submit: 'Отправить',
|
||||
Table: 'Таблица',
|
||||
Take_a_photo: 'Сфотографировать',
|
||||
Take_a_video: 'Записать видео',
|
||||
tap_to_change_status: 'нажмите для изменения статуса',
|
||||
Tap_to_view_servers_list: 'Нажмите, чтобы просмотреть список серверов',
|
||||
Terms_of_Service: ' Условия использования ',
|
||||
Theme: 'Тема',
|
||||
The_URL_is_invalid: 'IНеверный URL или невозможно установить безопасное соединение.\n{{contact}}',
|
||||
There_was_an_error_while_action: 'Произошла ошибка в процессе {{action}}!',
|
||||
This_room_is_blocked: 'Этот канал заблокирован',
|
||||
This_room_is_read_only: 'Этот канал доступен только для чтения',
|
||||
Thread: 'Тема',
|
||||
Threads: 'Темы',
|
||||
Timezone: 'Часовой пояс',
|
||||
To: 'К',
|
||||
topic: 'топик',
|
||||
Topic: 'Топик',
|
||||
Translate: 'Перевести',
|
||||
Try_again: 'Попробуйте еще раз',
|
||||
Two_Factor_Authentication: 'Двухфакторная аутентификация',
|
||||
Type_the_channel_name_here: 'Введите название канала здесь',
|
||||
unarchive: 'разархивировать',
|
||||
UNARCHIVE: 'РАЗАРХИВИРОВАТЬ',
|
||||
Unblock_user: 'Разблокировать пользователя',
|
||||
Unfavorite: 'Удалить из избранного',
|
||||
Unfollowed_thread: 'Отписаться',
|
||||
Unmute: 'Отменить заглушивание',
|
||||
unmuted: 'Заглушивание отменено',
|
||||
Unpin: 'Открепить',
|
||||
unread_messages: 'непрочитанные',
|
||||
Unread: 'Непрочитанные',
|
||||
Unread_on_top: 'Непрочитанные сверху',
|
||||
Unstar: 'Снять отметку',
|
||||
Updating: 'Обновление...',
|
||||
Uploading: 'Выгрузка',
|
||||
Upload_file_question_mark: 'Загрузить файл?',
|
||||
Users: 'Пользователи',
|
||||
User_added_by: 'Пользователь {{userAdded}} добавлен по решению {{userBy}}',
|
||||
User_has_been_key: 'Пользователь был {{key}}!',
|
||||
User_is_no_longer_role_by_: '{{user}} больше не {{role}} по решению {{userBy}}',
|
||||
User_muted_by: 'Пользователь {{userMuted}} заглушен по решению {{userBy}}',
|
||||
User_removed_by: 'Пользователь {{userRemoved}} удален по решению {{userBy}}',
|
||||
User_sent_an_attachment: '{{user}} отправил вложение',
|
||||
User_unmuted_by: 'Пользователь {{userUnmuted}} перестал быть заглушенным по решению {{userBy}}',
|
||||
User_was_set_role_by_: '{{user}} был назначен {{role}} пользователем {{userBy}}',
|
||||
Username_is_empty: 'Имя пользователя пусто',
|
||||
Username: 'Имя пользователя',
|
||||
Username_or_email: 'Имя пользователя или email',
|
||||
Validating: 'Проверка',
|
||||
Video_call: 'Видеозвонок',
|
||||
View_Original: 'Посмотреть оригинал',
|
||||
Voice_call: 'Голосовой вызов',
|
||||
Welcome: 'Добро пожаловать',
|
||||
Websocket_disabled: 'Websocket отключен для этого сервера.\n{{contact}}',
|
||||
Welcome: 'Добро пожаловать,',
|
||||
Welcome_to_RocketChat: 'Добро пожаловать в Rocket.Chat',
|
||||
Whats_your_2fa: 'Какой у вас код 2FA?',
|
||||
Without_Servers: 'Без серверов',
|
||||
Yes_action_it: 'Да, {{action}} это!',
|
||||
Yesterday: 'Вчера',
|
||||
You_are_in_preview_mode: 'Вы находитесь в режиме предварительного просмотра',
|
||||
|
@ -303,7 +434,12 @@ export default {
|
|||
You_can_search_using_RegExp_eg: 'Вы можете выполнить поиск с помощью регулярных выражений, например `/^text$/i`',
|
||||
You_colon: 'Вы: ',
|
||||
you_were_mentioned: 'вы были упомянуты',
|
||||
You_will_not_be_able_to_recover_this_message: 'Вы не сможете восстановить это сообщение!',
|
||||
you: 'вы',
|
||||
Your_server: 'Ваш сервер'
|
||||
You: 'Вы',
|
||||
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Вам нужно получить доступ как минимум к одному серверу Rocket.Chat, чтобы поделиться чем-то.',
|
||||
Your_certificate: 'Ваш сертификат',
|
||||
Version_no: 'Version: {{version}}',
|
||||
You_will_not_be_able_to_recover_this_message: 'Вы не сможете восстановить это сообщение!',
|
||||
Change_Language: 'Изменить язык',
|
||||
Crash_report_disclaimer: 'Мы никогда не отслеживаем содержание ваших чатов. Отчет о сбое содержит только важную для нас информацию для выявления проблем и их устранения.'
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue