Merge beta into master (#1897)

* [FIX] Close SortDropdown on sort select (#1230)

* [FIX] Cancel upload and check failed upload (#1232)

* [FIX] Slash commands not cleaning is typing and not using state (#1233)

* [FIX] Dispatch roomsRequest on app foreground event even if not connected (#1234)

* [CHORE] Update react-native-jitsi-meet (#1235)

* [FIX] Regex on run slash command (#1223)

* Update React Native to 0.61.1 (#1236)

* Update React Native to 0.61.1

* Update patch to SSL Pinning

* Revert storybook

* [CHORE] Update react-native-safe-area-view (#1219)

* [FIX] Try/catch JSON.parse XHR response (#1238)

* [FIX] Change messagebox icon immediate on change text (#1241)

* [FIX] Update last open on message stream received (#1240)

* [FIX] Remove animation from RoomsListView.willFocus (#1239)

* [FIX] Delete message on thread (#1214)

* [REGRESSION] Markdown text (#1242)

* [FIX] Jest (#1243)

* [FIX] Avatar shown when useRealName is activated (#1162)

* Fix avatar when use real name

* Wrong indentation

* [DOCS] Add SECURITY.md (#1244)

* [CHORE] Update react-native-reanimated to 1.3.0 (#1246)

* [FIX] Run credentials migration only once (#1245)

* [CHORE] Update react-native-jitsi-meet to 2.0.1 (#1249)

* [FIX] Messagebox onChangeText issues (#1252)

* Stop ongoing debounces on messagebox unmount

* Immediately change send icon, but keep debouncing others

* Make CustomEmoji stateless function

* Fix mentions keyExtractor

* [FIX] Room subscription issues (#1255)

* [FIX] Reaction press (#1258)

* [FIX] Channel avatars not showing after application unloads (#1264)

* Revert react-native-safe-area-view (#1265)

* [FIX] Remove console on production mode (#1268)

* [FIX] Messages preview issues (#1257)

* [FIX] Select user from native credentials (#1266)

* [FIX] Some issues on preview message (#1271)

* [FIX] Audio player track and thumb not rendering on Android (#1273)

* [FIX] Record audio message throws exception when FileSystem.getInfoAsync is called (#1272)

* [FIX] China shouldn't use CallKit (#1274)

* [FIX] Watermelon batches (#1277)

* Bump version to 1.20.1 (#1285)

* [CHORE] Remove memoize-one (#1284)

* [FIX] End Jitsi call on unmount (#1291)

* [FIX] Allow self-signed certificates (#1310)

* [FIX] Set User-Agent  (#1318)

* Set User-Agent Fetch & Websocket & XHR

* Set User-Agent

* Custom User Agent on fetch/websocket

* Fix names

* Use DeviceInfo

* fix server with subpath (#1322)

* [FIX] Server with https:\\ instead of https:// (#1320)

* [FIX] Server dropdown not closing after changing stack (#1299)

* [FIX] Invalid server version (#1319)

* [IMPROVEMENT] Respect "Hide counter" preference (#1306)

* [FIX] Pass isFocused as a function to Messagebox (#1309)

* [CHORE] Remove icons folder (#1290)

* [CHORE] Refactor RoomItem touchable (#1331)

* [FIX] Unnecessary rerender on RoomItem when status is undefined (#1336)

* [UPDATE DEPS] react-navigation and react-navigation-stack (#1337)

* [FIX] Avatars not loading on share extension when Accounts_AvatarBlockUnauthenticatedAccess is enabled (#1339)

* Bump version to 1.20.2 (#1340)

* [FIX] Remove some unnecessary re-renders on Messagebox (#1341)

* [REGRESSION] Use LayoutAnimation instead of Transition API (#1338)

* [FIX] Remove setState from notifications view causing watermelon object to be updated outside an action (#1342)

* [IMPROVEMENT] Save last message as message when subscription is updated (#1344)

* [UPDATE DEPS] Update RN to 0.61.3 (#1345)

* [DOCS] Update Readme (#1346)

* [CHORE] Remove react-native-scrollable-tab-view fork (#1352)

* [FIX] URL preview (#1360)

* [REGRESSION] Decrease list view memory size (#1361)

* [FIX] Paste (#1350)

* [CHORE] Update gems (#1365)

* Bump version to 1.20.3 (#1366)

* [FIX] Use Ruby 2.4 on TestFlight upload (#1368)

* [FIX] Parse Urls (#1371)

* [FIX] Parse image URL only if it's not empty (#1372)

* [FIX] Load messages issues (#1373)

* Bump version to 1.21.0 (#1376)

* [FIX] Crowd login (#1381)

* [FIX] Clicking user avatar in thread previews crashes app (#1363)

* [IMPROVEMENT] Error messages on connect (#1379)

* [FIX] ProfileView input navigation error when custom fields aren't set (#1383)

* [FIX] Batch server deletion on logout (#1382)

* Bump app to 1.22.0 (#1387)

* [FIX] Server Version (#1392)

* Update patch and minor deps (#1386)

* [FIX] Crash when open thread (#1395)

* Bump version to 1.23.0 (#1394)

* [I18N] Update ru.js (#1384)

* [FIX] CAS building wrong URL (#1362)

* [FIX] Delete messages (#1399)

* [FIX] In-app notification showing wrong content on channels (#1400)

* Bump version to 1.24.0 (#1404)

* [FIX] Prevent server with whitespace (#1402)

* [IMPROVEMENT] Keyboard and content type on login (#1403)

* [FIX] Messages stop loading (#1410)

* [NEW] Tablet support (#1300)

* [IMPROVEMENT] Authentication via deep linking (#1418)

* [IMPROVEMENT] Markdown performance when identifying emoji only content (#1422)

* [FIX] BackHandler remove random failing on development (#1423)

* Bump version to 1.25.0 (#1424)

* [CHORE] Update CI Xcode Image (#1430)

* [FIX] Rooms grouping not working properly (#1435)

* [FIX] Take a video (#1437)

* [NEW] Themes (#1298)

* [FIX] Share extension doesn't reconnect to previous selected server on Android (#1429)

* [FIX] Init local settings on notification tap (#1438)

* Bump version to 1.26.0 (#1450)

* [FIX] Emoji parser not working on Hermes  (#1445)

* [NEW] Enable Hermes (#1446)

* [FIX] Automatic theme repeating (#1457)

* [CHORE] Sync Experimental and Official app versions (#1458)

* [DOCS] Update readme (#1459)

* [FIX] Messages being sent but showing as temp status (#1469)

* [FIX] Missing messages after reconnect (#1470)

* [FIX] Few fixes on themes (#1477)

* [I18N] Missing German translations (#1465)

* Missing German translation

* adding a missing space behind colon

* added a missing space after colon

* and another attempt to finally fix this – got confused by all the branches

* some smaller fixes for the translation

* better wording

* fixed another typo

* [FIX] Crash while displaying the attached image with http on file name (#1401)

* [IMPROVEMENT] Tap app and server version to copy to clipboard (#1425)

* [NEW] Reply notification (#1448)

* [FIX] Incorrect background color login on iPad (#1480)

* [FIX] Prevent multiple tap on send (Share Extension) (#1481)

* [NEW] Image Viewer (#1479)

* [DOCS] Update Readme (#1485)

* [FIX] Jitsi with Hermes Enabled (#1523)

* [FIX] Draft messages not working with themed Messagebox (#1525)

* [FIX] Go to direct message from members list (#1519)

* [FIX] Make SAML wait for idp token instead of creating it on client (#1527)

* [FIX] Server Test Push Notification (#1508)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Update to new server response (#1509)

* [FIX] Insert messages with blank users (#1529)

* Bump version to 4.2.1 (#1530)

* [FIX] Error when normalizing empty messages (#1532)

* [REGRESSION] CAS (#1570)

* Bump version to 4.2.2 (#1571)

* [FIX] Add username block condition to prevent error (#1585)

* Bump version to 4.2.3

* Bump version to 4.2.4

* Bump version to 4.3.0 (#1630)

* [FIX] Channels doesn't load (#1586)

* [FIX] Channels doesn't load

* [FIX] Update roomsUpdatedAt when subscriptions.length is 0

* [FIX] Remove unnecessary changes

* [FIX] Improve the code

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Make SAML to work on Rocket.Chat < 2.3.0 (#1629)

* [NEW] Invite links (#1534)

* [FIX] Set the http-agent to the form that Rocket.Chat requires for logging (#1482)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] "Following thread" and "Unfollowed Thread" is hardcoded and not translated (#1625)

* [FIX] Disable reset button if form didn't changed (#1569)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Header title of RoomInfoView (#1553)

* [I18N] Gallery Permissions DE (#1542)

* [FIX] Not allow to send messages to archived room (#1623)

* [FIX] Profile fields automatically reset (#1502)

* [FIX] Show attachment on ThreadMessagesView (#1493)

* [NEW] Wordpress auth (#1633)

* [CHORE] Add Start Packager script (#1639)

* [CHORE] Update RN to 0.61.5 (#1638)

* [CHORE] Update RN to 0.61.5

* [CHORE] Update react-native patch

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* Bump version to 4.3.1 (#1641)

* [FIX] Change force logout rule (#1640)

* Bump version to 4.4.0 (#1643)

* [IMPROVEMENT] Use MessagingStyle on Android Notification (#1575)

* [NEW] Request review (#1627)

* [NEW] Pull to refresh RoomView (#1657)

* [FIX] Unsubscribe from room (#1655)

* [FIX] Server with subdirs (#1646)

* [NEW] Clear cache (#1660)

* [IMPROVEMENT] Memoize and batch subscriptions updates (#1642)

* [FIX] Disallow empty sharing (#1664)

* [REGRESSION] Use HTTPS links for sharing and markets protocol for review (#1663)

* [FIX] In some cases, share extension doesn't load images (#1649)

* [i18n] DE translations for new invite function and some minor fixes (#1631)

* [FIX] Remove duplicate jetify step (#1628)

minor: also remove 'cd' calls

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [REGRESSION] Read messages (#1666)

* [i18n] German translations missing (#1670)

* [FIX] Notifications crash on older Android Versions (#1672)

* [i18n] Added Dutch translation (#1676)

* [NEW] Omnichannel Beta (#1674)

* [NEW] Confirm logout/clear cache (#1688)

* [I18N] Add es-ES language  (#1495)

* [NEW] UiKit Beta (#1497)

* [IMPROVEMENT] Use reselect (#1696)

* [FIX] Notification in Android API level less than 24 (#1692)

* [IMPROVEMENT] Send tmid on slash commands and media (#1698)

* [FIX] Unhandled action on UIKit (#1703)

* [NEW] Pull to refresh RoomsList (#1701)

* [IMPROVEMENT] Reset app when language is changed (#1702)

* [FIX] Small fixes on UIKit (#1709)

* [FIX] Spotlight (#1719)

* [CHORE] Update react-native-image-crop-picker (#1712)

* [FIX] Messages Overlapping (Android) and MessageBox Scroll (iOS) (#1720)

* [REGRESSION] Remove @ and # from mention (#1721)

* [NEW] Direct message from user info (#1516)

* [FIX] Delete slash commands (#1723)

* [IMPROVEMENT] Hold URL to copy (#1684)

* [FIX] Different sourcemaps generation for Hermes (#1724)

* [FIX] Different sourcemaps generation for Hermes

* Upload sourcemaps after build

* [REVERT] Show emoji keyboard on Android (#1738)

* [FIX] Stop logging react-native-image-crop-picker (#1745)

* [FIX] Prevent toast ref error (#1744)

* [FIX] Prevent reaction map error (#1743)

* [FIX] Add missing calls to user info (#1741)

* [FIX] Catch room unsubscribe error (#1739)

* [i18n] Missing German keys (#1735)

* [FIX] Missing i18n on MessagesView title (#1733)

* [FIX]  UIKit Modal: Weird behavior on Android Tablet (#1742)

* [i18n] Missing key on German (#1747)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Add Italian (#1736)

* [CHORE] Memory leaks investigation (#1675)

* [IMPROVEMENT] Alert verify email when enabled (#1725)

* [NEW] Jitsi JWT added to URL (#1746)

* [FIX] UIKit submit when connection lost (#1748)

* Bump version to 4.5.0 (#1761)

* [NEW] Default browser (#1752)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] HTTP Basic Auth (#1753)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Honor profile fields edit settings (#1687)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Room announcements (#1726)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Honor Register/Login settings (#1727)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Make links clickable on Room Info (#1730)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Hide system messages (#1755)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Honor "Message_AudioRecorderEnabled" (#1764)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Missing de keys (#1765)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Redirect user to SetUsernameView (#1728)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Join Room (#1769)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Accept all media types using * (#1770)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Use RealName when necessary (#1758)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Markdown Line Break (#1783)

* [IMPROVEMENT] Remove useMarkdown (#1774)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Open browser rather than webview on Create Workspace (#1788)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Markdown perf (#1796)

* [FIX] Stop video when modal is closed (#1787)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Hide reply notification action when there are missing data (#1771)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Added Japanese translation (#1781)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Reset password error message (#1772)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Close tablet modal (#1773)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Setting not present (#1775)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Thread header (#1776)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Keyboard tracking loses input ref (#1784)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Mark message as unread (#1785)

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [IMPROVEMENT] Log server version (#1786)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Add loading message on long running tasks (#1798)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Switch Apple account on Fastlane (#1810)

* [FIX] Watermelon throwing "Cannot update a record with pending updates" (#1754)

* [FIX] Detox tests (#1790)

* [CHORE] Use markdown preview on RoomView Header (#1807)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] LoginSignup blink services (#1809)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Request user presence on demand (#1813)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Remove all invited users when create a channel (#1814)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Pop from room which you have been removed (#1819)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Room Info styles (#1820)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Add missing German keys (#1800)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Empty mentions for @all and @here when real name is enabled (#1822)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Markdown added to Storybook (#1812)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [REGRESSION] Room View header title (#1827)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Storybook snapshots (#1831)

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [FIX] Mentions (#1829)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Thread message not found (#1830)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Separate delete and remove channel (#1832)

* Rename to delete room

* Separate delete and remove channel

* handleRemoved -> handleRoomRemoved

* [FIX] Navigate to RoomsList & Handle tablet case

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [NEW] Filter system messages per room (#1815)

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] e2e tests (#1838)

* [FIX] Consecutive clear cache calls freezing app (#1851)

* Bump version to 4.5.1 (#1853)

* [FIX][iOS] Ignore silent mode on audio player (#1862)

* [IMPROVEMENT] Create App Group property on Info.plist (#1858)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

Co-authored-by: Prateek Jain <44807945+Prateek93a@users.noreply.github.com>
Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Lucas Siqueira <lucassiqzro@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>
Co-authored-by: Pitstopper <18574776+Pitstopper@users.noreply.github.com>
Co-authored-by: phriedrich <info@phriedrich.de>
Co-authored-by: Guilherme Siqueira <guilhersiqueira@gmail.com>
Co-authored-by: Prateek Jain <prateek93a@gmail.com>
Co-authored-by: devyaniChoubey <52153085+devyaniChoubey@users.noreply.github.com>
Co-authored-by: Bernard Seow <ssbing99@gmail.com>
Co-authored-by: Hiroki Ishiura <ishiura@ja2.so-net.ne.jp>
Co-authored-by: Exordian <jakob.englisch@gmail.com>
Co-authored-by: Daanchaam <daanhendriks97@gmail.com>
Co-authored-by: Youssef Muhamad <emaildeyoussefmuhamad@gmail.com>
Co-authored-by: Iván Álvarez <ialvarezpereira@gmail.com>
Co-authored-by: Sarthak Pranesh <41206172+sarthakpranesh@users.noreply.github.com>
Co-authored-by: Michele Pellegrini <pellettiero@users.noreply.github.com>
Co-authored-by: Tanmoy Bhowmik <tanmoy.openroot@gmail.com>
Co-authored-by: Hibikine Kage <14365761+hibikine@users.noreply.github.com>
This commit is contained in:
Diego Mello 2020-03-18 18:26:17 -03:00 committed by GitHub
parent 69aff7e56a
commit 96bfe1910e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 7263 additions and 2246 deletions

View File

@ -44,7 +44,7 @@ jobs:
paths:
- ./node_modules
e2e-test:
e2e-build:
macos:
xcode: "11.2.1"
@ -80,11 +80,73 @@ jobs:
yarn global add detox-cli
yarn
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run:
name: Build
command: |
detox build --configuration ios.sim.release
- persist_to_workspace:
root: .
paths:
- ios/build/Build/Products/Release-iphonesimulator/RocketChatRN.app
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
e2e-test:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
steps:
- checkout
- attach_workspace:
at: .
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
- run:
name: Install NPM modules
command: |
yarn global add detox-cli
yarn
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run:
name: Test
command: |
@ -96,9 +158,6 @@ jobs:
paths:
- node_modules
- store_artifacts:
path: /tmp/screenshots
android-build:
<<: *defaults
docker:
@ -359,9 +418,12 @@ workflows:
type: approval
requires:
- lint-testunit
- e2e-test:
- e2e-build:
requires:
- e2e-hold
- e2e-test:
requires:
- e2e-build
- ios-build:
requires:

View File

@ -104,7 +104,7 @@ Readme will guide you on how to config.
| Report message | ✅ |
| Theming | ✅ |
| Settings -> Review the App | ✅ |
| Settings -> Default Browser | |
| Settings -> Default Browser | |
| Admin panel | ✅ |
| Reply message from notification | ✅ |
| Unread counter banner on message list | ✅ |

View File

@ -0,0 +1,4 @@
export default {
set: () => '',
get: () => ''
};

File diff suppressed because it is too large Load Diff

View File

@ -138,7 +138,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.4.0"
versionName "4.5.1"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
}

View File

@ -252,7 +252,9 @@ public class CustomPushNotification extends PushNotification {
}
private void notificationReply(Notification.Builder notification, int notificationId, Bundle bundle) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
String notId = bundle.getString("notId", "1");
String ejson = bundle.getString("ejson", "{}");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notId.equals("1") || ejson.equals("{}")) {
return;
}
String label = "Reply";

View File

@ -30,7 +30,7 @@ public class Ejson {
public String serverURL() {
String url = this.host;
if (url.endsWith("/")) {
if (url != null && url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'DELETE_INIT', 'DELETE_FINISH', 'USER_TYPING']);
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]);
@ -50,7 +50,6 @@ export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPE
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';

View File

@ -1,10 +1,11 @@
import * as types from '../constants/types';
import { APP } from './actionsTypes';
export function appStart(root) {
export function appStart(root, text) {
return {
type: APP.START,
root
root,
text
};
}
@ -39,12 +40,6 @@ export function addSettings(settings) {
payload: settings
};
}
export function setAllSettings(settings) {
return {
type: types.SET_ALL_SETTINGS,
payload: settings
};
}
export function login() {
return {

View File

@ -1,8 +0,0 @@
import * as types from './actionsTypes';
export function toggleMarkdown(value) {
return {
type: types.TOGGLE_MARKDOWN,
payload: value
};
}

View File

@ -8,14 +8,20 @@ export function leaveRoom(rid, t) {
};
}
export function eraseRoom(rid, t) {
export function deleteRoomInit(rid, t) {
return {
type: types.ROOM.ERASE,
type: types.ROOM.DELETE_INIT,
rid,
t
};
}
export function deleteRoomFinish() {
return {
type: types.ROOM.DELETE_FINISH
};
}
export function userTyping(rid, status = true) {
return {
type: types.ROOM.USER_TYPING,

View File

@ -1,4 +1,22 @@
export default {
Accounts_AllowEmailChange: {
type: 'valueAsBoolean'
},
Accounts_AllowPasswordChange: {
type: 'valueAsBoolean'
},
Accounts_AllowRealNameChange: {
type: 'valueAsBoolean'
},
Accounts_AllowUserAvatarChange: {
type: 'valueAsBoolean'
},
Accounts_AllowUserProfileChange: {
type: 'valueAsBoolean'
},
Accounts_AllowUsernameChange: {
type: 'valueAsBoolean'
},
Accounts_CustomFields: {
type: 'valueAsString'
},
@ -17,12 +35,24 @@ export default {
Accounts_PasswordReset: {
type: 'valueAsBoolean'
},
Accounts_RegistrationForm: {
type: 'valueAsString'
},
Accounts_RegistrationForm_LinkReplacementText: {
type: 'valueAsString'
},
Accounts_ShowFormLogin: {
type: 'valueAsBoolean'
},
CROWD_Enable: {
type: 'valueAsBoolean'
},
FEDERATION_Enabled: {
type: 'valueAsBoolean'
},
Hide_System_Messages: {
type: 'valueAsArray'
},
LDAP_Enable: {
type: 'valueAsBoolean'
},
@ -59,6 +89,9 @@ export default {
Message_AllowStarring: {
type: 'valueAsBoolean'
},
Message_AudioRecorderEnabled: {
type: 'valueAsBoolean'
},
Message_GroupingPeriod: {
type: 'valueAsNumber'
},
@ -93,7 +126,7 @@ export default {
type: 'valueAsBoolean'
},
Threads_enabled: {
type: null
type: 'valueAsBoolean'
},
FileUpload_MediaTypeWhiteList: {
type: 'valueAsString'

View File

@ -1,4 +1,3 @@
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const ADD_SETTINGS = 'ADD_SETTINGS';

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Touch from '../utils/touch';
const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => (
@ -45,6 +46,7 @@ const Avatar = React.memo(({
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>

View File

@ -63,6 +63,12 @@ class MessageActions extends React.Component {
this.EDIT_INDEX = this.options.length - 1;
}
// Mark as unread
if (message.u && message.u._id !== user.id) {
this.options.push(I18n.t('Mark_unread'));
this.UNREAD_INDEX = this.options.length - 1;
}
// Permalink
this.options.push(I18n.t('Permalink'));
this.PERMALINK_INDEX = this.options.length - 1;
@ -243,6 +249,30 @@ class MessageActions extends React.Component {
editInit(message);
}
handleUnread = async() => {
const { message, room } = this.props;
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.collections.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async() => {
try {
await subRecord.update(sub => sub.lastOpen = ts);
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
log(e);
}
}
handleCopy = async() => {
const { message } = this.props;
await Clipboard.setString(message.msg);
@ -349,6 +379,9 @@ class MessageActions extends React.Component {
case this.EDIT_INDEX:
this.handleEdit();
break;
case this.UNREAD_INDEX:
this.handleUnread();
break;
case this.PERMALINK_INDEX:
this.handlePermalink();
break;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { FlatList } from 'react-native';
import { FlatList, View } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
@ -12,15 +12,16 @@ const Mentions = React.memo(({ mentions, trackingType, theme }) => {
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'
/>
<View testID='messagebox-container'>
<FlatList
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'
/>
</View>
);
}, (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {

View File

@ -42,7 +42,7 @@ const styles = StyleSheet.create({
});
const ReplyPreview = React.memo(({
message, Message_TimeFormat, baseUrl, username, useMarkdown, replying, getCustomEmoji, close, theme
message, Message_TimeFormat, baseUrl, username, replying, getCustomEmoji, close, theme
}) => {
if (!replying) {
return null;
@ -67,7 +67,6 @@ const ReplyPreview = React.memo(({
username={username}
getCustomEmoji={getCustomEmoji}
numberOfLines={1}
useMarkdown={useMarkdown}
preview
theme={theme}
/>
@ -79,7 +78,6 @@ const ReplyPreview = React.memo(({
ReplyPreview.propTypes = {
replying: PropTypes.bool,
useMarkdown: PropTypes.bool,
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
@ -90,7 +88,6 @@ ReplyPreview.propTypes = {
};
const mapStateToProps = state => ({
useMarkdown: state.markdown.useMarkdown,
Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.server.server
});

View File

@ -4,17 +4,20 @@ import PropTypes from 'prop-types';
import { SendButton, AudioButton, FileButton } from './buttons';
const RightButtons = React.memo(({
theme, showSend, submit, recordAudioMessage, showFileActions
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showFileActions
}) => {
if (showSend) {
return <SendButton onPress={submit} theme={theme} />;
}
return (
<>
<AudioButton onPress={recordAudioMessage} theme={theme} />
<FileButton onPress={showFileActions} theme={theme} />
</>
);
if (recordAudioMessageEnabled) {
return (
<>
<AudioButton onPress={recordAudioMessage} theme={theme} />
<FileButton onPress={showFileActions} theme={theme} />
</>
);
}
return <FileButton onPress={showFileActions} theme={theme} />;
});
RightButtons.propTypes = {
@ -22,6 +25,7 @@ RightButtons.propTypes = {
showSend: PropTypes.bool,
submit: PropTypes.func.isRequired,
recordAudioMessage: PropTypes.func.isRequired,
recordAudioMessageEnabled: PropTypes.bool,
showFileActions: PropTypes.func.isRequired
};

View File

@ -4,19 +4,23 @@ import PropTypes from 'prop-types';
import { SendButton, AudioButton } from './buttons';
const RightButtons = React.memo(({
theme, showSend, submit, recordAudioMessage
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled
}) => {
if (showSend) {
return <SendButton theme={theme} onPress={submit} />;
}
return <AudioButton theme={theme} onPress={recordAudioMessage} />;
if (recordAudioMessageEnabled) {
return <AudioButton theme={theme} onPress={recordAudioMessage} />;
}
return null;
});
RightButtons.propTypes = {
theme: PropTypes.string,
showSend: PropTypes.bool,
submit: PropTypes.func.isRequired,
recordAudioMessage: PropTypes.func.isRequired
recordAudioMessage: PropTypes.func.isRequired,
recordAudioMessageEnabled: PropTypes.bool
};
export default RightButtons;

View File

@ -85,13 +85,15 @@ class MessageBox extends Component {
replyWithMention: PropTypes.bool,
FileUpload_MediaTypeWhiteList: PropTypes.string,
FileUpload_MaxFileSize: PropTypes.number,
Message_AudioRecorderEnabled: PropTypes.bool,
getCustomEmoji: PropTypes.func,
editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
typing: PropTypes.func,
theme: PropTypes.string,
replyCancel: PropTypes.func
replyCancel: PropTypes.func,
navigation: PropTypes.object
}
constructor(props) {
@ -139,7 +141,7 @@ class MessageBox extends Component {
async componentDidMount() {
const db = database.active;
const { rid, tmid } = this.props;
const { rid, tmid, navigation } = this.props;
let msg;
try {
const threadsCollection = db.collections.get('threads');
@ -177,6 +179,12 @@ class MessageBox extends Component {
if (isTablet) {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
}
this.didFocusListener = navigation.addListener('didFocus', () => {
if (this.tracking && this.tracking.resetTracking) {
this.tracking.resetTracking();
}
});
}
componentWillReceiveProps(nextProps) {
@ -257,6 +265,9 @@ class MessageBox extends Component {
if (this.getSlashCommands && this.getSlashCommands.stop) {
this.getSlashCommands.stop();
}
if (this.didFocusListener && this.didFocusListener.remove) {
this.didFocusListener.remove();
}
if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
}
@ -781,7 +792,7 @@ class MessageBox extends Component {
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
} = this.state;
const {
editing, message, replying, replyCancel, user, getCustomEmoji, theme
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled
} = this.props;
const isAndroidTablet = isTablet && isAndroid ? {
@ -842,6 +853,7 @@ class MessageBox extends Component {
showSend={showSend}
submit={this.submit}
recordAudioMessage={this.recordAudioMessage}
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
showFileActions={this.showFileActions}
/>
</View>
@ -864,6 +876,7 @@ class MessageBox extends Component {
}}
>
<KeyboardAccessoryView
ref={ref => this.tracking = ref}
renderContent={this.renderContent}
kbInputRef={this.component}
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
@ -891,7 +904,8 @@ const mapStateToProps = state => ({
threadsEnabled: state.settings.Threads_enabled,
user: getUserSelector(state),
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize,
Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled
});
const dispatchToProps = ({

View File

@ -20,7 +20,7 @@ const Chip = ({ item, onSelect, theme }) => (
>
<>
{item.imageUrl ? <Image style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
<Text style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
<CustomIcon name='cross' size={16} color={themes[theme].auxiliaryText} />
</>
</Touchable>

View File

@ -41,6 +41,12 @@ export const MultiSelect = React.memo(({
const [currentValue, setCurrentValue] = useState('');
const [showContent, setShowContent] = useState(false);
useEffect(() => {
if (values) {
select(values);
}
}, [values]);
useEffect(() => {
setOpen(showContent);
}, [showContent]);

View File

@ -34,6 +34,7 @@ export default StyleSheet.create({
},
item: {
height: 48,
maxWidth: '85%',
alignItems: 'center',
flexDirection: 'row'
},
@ -59,7 +60,7 @@ export default StyleSheet.create({
chips: {
flexDirection: 'row',
flexWrap: 'wrap',
marginRight: 16
marginRight: 50
},
chip: {
flexDirection: 'row',
@ -72,6 +73,7 @@ export default StyleSheet.create({
},
chipText: {
paddingHorizontal: 8,
flexShrink: 1,
...sharedStyles.textMedium,
fontSize: 14
},

View File

@ -7,7 +7,7 @@ import { themes } from '../../constants/colors';
import styles from './styles';
const AtMention = React.memo(({
mention, mentions, username, navToRoomInfo, preview, style = [], theme
mention, mentions, username, navToRoomInfo, style = [], useRealName, theme
}) => {
let mentionStyle = { ...styles.mention, color: themes[theme].buttonText };
if (mention === 'all' || mention === 'here') {
@ -27,22 +27,23 @@ const AtMention = React.memo(({
};
}
const user = mentions && mentions.length && mentions.find(m => m.username === mention);
const handlePress = () => {
const index = mentions.findIndex(m => m.username === mention);
const navParam = {
t: 'd',
rid: mentions[index]._id
rid: user && user._id
};
navToRoomInfo(navParam);
};
if (mentions && mentions.length && mentions.findIndex(m => m.username === mention) !== -1) {
if (user) {
return (
<Text
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : mentionStyle, ...style]}
onPress={preview ? undefined : handlePress}
style={[mentionStyle, ...style]}
onPress={handlePress}
>
{mention}
{useRealName && user.name ? user.name : user.username}
</Text>
);
}
@ -59,7 +60,7 @@ AtMention.propTypes = {
username: PropTypes.string,
navToRoomInfo: PropTypes.func,
style: PropTypes.array,
preview: PropTypes.bool,
useRealName: PropTypes.bool,
theme: PropTypes.string,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};

View File

@ -7,7 +7,7 @@ import { themes } from '../../constants/colors';
import styles from './styles';
const Hashtag = React.memo(({
hashtag, channels, navToRoomInfo, preview, style = [], theme
hashtag, channels, navToRoomInfo, style = [], theme
}) => {
const handlePress = () => {
const index = channels.findIndex(channel => channel.name === hashtag);
@ -21,8 +21,8 @@ const Hashtag = React.memo(({
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
return (
<Text
style={[preview ? { ...styles.text, color: themes[theme].bodyText } : styles.mention, ...style]}
onPress={preview ? undefined : handlePress}
style={[styles.mention, ...style]}
onPress={handlePress}
>
{hashtag}
</Text>
@ -39,7 +39,6 @@ Hashtag.propTypes = {
hashtag: PropTypes.string,
navToRoomInfo: PropTypes.func,
style: PropTypes.array,
preview: PropTypes.bool,
theme: PropTypes.string,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};

View File

@ -10,7 +10,7 @@ import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
const Link = React.memo(({
children, link, preview, theme
children, link, theme
}) => {
const handlePress = () => {
if (!link) {
@ -28,13 +28,9 @@ const Link = React.memo(({
// if you have a [](https://rocket.chat) render https://rocket.chat
return (
<Text
onPress={preview ? undefined : handlePress}
onLongPress={preview ? undefined : onLongPress}
style={
!preview
? { ...styles.link, color: themes[theme].actionTintColor }
: { color: themes[theme].bodyText }
}
onPress={handlePress}
onLongPress={onLongPress}
style={{ ...styles.link, color: themes[theme].actionTintColor }}
>
{ childLength !== 0 ? children : link }
</Text>
@ -44,8 +40,7 @@ const Link = React.memo(({
Link.propTypes = {
children: PropTypes.node,
link: PropTypes.string,
theme: PropTypes.string,
preview: PropTypes.bool
theme: PropTypes.string
};
export default Link;

View File

@ -3,6 +3,7 @@ import { Text, Image } from 'react-native';
import { Parser, Node } from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import PropTypes from 'prop-types';
import removeMarkdown from 'remove-markdown';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import I18n from '../../i18n';
@ -18,6 +19,7 @@ import MarkdownEmoji from './Emoji';
import MarkdownTable from './Table';
import MarkdownTableRow from './TableRow';
import MarkdownTableCell from './TableCell';
import mergeTextNodes from './mergeTextNodes';
import styles from './styles';
@ -71,22 +73,23 @@ class Markdown extends PureComponent {
tmid: PropTypes.string,
isEdited: PropTypes.bool,
numberOfLines: PropTypes.number,
useMarkdown: PropTypes.bool,
customEmojis: PropTypes.bool,
useRealName: PropTypes.bool,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
navToRoomInfo: PropTypes.func,
preview: PropTypes.bool,
theme: PropTypes.string,
testID: PropTypes.string,
style: PropTypes.array
};
constructor(props) {
super(props);
this.renderer = this.createRenderer(props.preview);
this.renderer = this.createRenderer();
}
createRenderer = (preview = false) => new Renderer({
createRenderer = () => new Renderer({
renderers: {
text: this.renderText,
@ -119,7 +122,7 @@ class Markdown extends PureComponent {
table_row: this.renderTableRow,
table_cell: this.renderTableCell,
editedIndicator: preview ? () => null : this.renderEditedIndicator
editedIndicator: this.renderEditedIndicator
},
renderParagraphsInLists: true
});
@ -141,19 +144,16 @@ class Markdown extends PureComponent {
renderText = ({ context, literal }) => {
const {
numberOfLines, preview, style = []
numberOfLines, style = []
} = this.props;
const defaultStyle = [
this.isMessageContainsOnlyEmoji && !preview ? styles.textBig : {},
this.isMessageContainsOnlyEmoji ? styles.textBig : {},
...context.map(type => styles[type])
];
return (
<Text
style={[
styles.text,
!preview ? defaultStyle : {},
...style
]}
accessibilityLabel={literal}
style={[styles.text, defaultStyle, ...style]}
numberOfLines={numberOfLines}
>
{literal}
@ -162,18 +162,16 @@ class Markdown extends PureComponent {
}
renderCodeInline = ({ literal }) => {
const { preview, theme, style = [] } = this.props;
const { theme, style = [] } = this.props;
return (
<Text
style={[
!preview
? {
...styles.codeInline,
color: themes[theme].bodyText,
backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme].bannerBackground
}
: { ...styles.text, color: themes[theme].bodyText },
{
...styles.codeInline,
color: themes[theme].bodyText,
backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme].bannerBackground
},
...style
]}
>
@ -183,18 +181,16 @@ class Markdown extends PureComponent {
};
renderCodeBlock = ({ literal }) => {
const { preview, theme, style = [] } = this.props;
const { theme, style = [] } = this.props;
return (
<Text
style={[
!preview
? {
...styles.codeBlock,
color: themes[theme].bodyText,
backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme].bannerBackground
}
: { ...styles.text, color: themes[theme].bodyText },
{
...styles.codeBlock,
color: themes[theme].bodyText,
backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme].bannerBackground
},
...style
]}
>
@ -221,11 +217,10 @@ class Markdown extends PureComponent {
};
renderLink = ({ children, href }) => {
const { preview, theme } = this.props;
const { theme } = this.props;
return (
<MarkdownLink
link={href}
preview={preview}
theme={theme}
>
{children}
@ -235,14 +230,13 @@ class Markdown extends PureComponent {
renderHashtag = ({ hashtag }) => {
const {
channels, navToRoomInfo, style, preview, theme
channels, navToRoomInfo, style, theme
} = this.props;
return (
<MarkdownHashtag
hashtag={hashtag}
channels={channels}
navToRoomInfo={navToRoomInfo}
preview={preview}
theme={theme}
style={style}
/>
@ -251,15 +245,15 @@ class Markdown extends PureComponent {
renderAtMention = ({ mentionName }) => {
const {
username, mentions, navToRoomInfo, preview, style, theme
username, mentions, navToRoomInfo, useRealName, style, theme
} = this.props;
return (
<MarkdownAtMention
mentions={mentions}
mention={mentionName}
useRealName={useRealName}
username={username}
navToRoomInfo={navToRoomInfo}
preview={preview}
theme={theme}
style={style}
/>
@ -268,13 +262,13 @@ class Markdown extends PureComponent {
renderEmoji = ({ emojiName, literal }) => {
const {
getCustomEmoji, baseUrl, customEmojis = true, preview, style, theme
getCustomEmoji, baseUrl, customEmojis = true, style, theme
} = this.props;
return (
<MarkdownEmoji
emojiName={emojiName}
literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji && !preview}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
customEmojis={customEmojis}
@ -335,10 +329,7 @@ class Markdown extends PureComponent {
};
renderBlockQuote = ({ children }) => {
const { preview, theme } = this.props;
if (preview) {
return children;
}
const { theme } = this.props;
return (
<MarkdownBlockQuote theme={theme}>
{children}
@ -367,7 +358,7 @@ class Markdown extends PureComponent {
render() {
const {
msg, useMarkdown = true, numberOfLines, preview = false, theme
msg, numberOfLines, preview = false, theme, style = [], testID
} = this.props;
if (!msg) {
@ -379,23 +370,22 @@ class Markdown extends PureComponent {
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
m = m.replace(/^\[([\s]]*)\]\(([^)]*)\)\s/, '').trim();
m = shortnameToUnicode(m);
if (preview) {
m = m.split('\n').reduce((lines, line) => `${ lines } ${ line }`, '');
const ast = parser.parse(m);
return this.renderer.render(ast);
m = m.replace(/\n+/g, ' ');
m = shortnameToUnicode(m);
m = removeMarkdown(m);
return (
<Text accessibilityLabel={m} style={[styles.text, { color: themes[theme].bodyText }, ...style]} numberOfLines={numberOfLines} testID={testID}>
{m}
</Text>
);
}
if (!useMarkdown && !preview) {
return <Text style={[styles.text, { color: themes[theme].bodyText }]} numberOfLines={numberOfLines}>{m}</Text>;
}
const ast = parser.parse(m);
let ast = parser.parse(m);
ast = mergeTextNodes(ast);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
this.editedMessage(ast);
return this.renderer.render(ast);
}
}

View File

@ -0,0 +1,27 @@
// TODO: should we add this to our commonmark fork instead?
// we loop through nodes and try to merge all texts
export default function mergeTextNodes(ast) {
// https://github.com/commonmark/commonmark.js/blob/master/lib/node.js#L268
const walker = ast.walker();
let event;
// eslint-disable-next-line no-cond-assign
while (event = walker.next()) {
const { entering, node } = event;
const { type } = node;
if (entering && type === 'text') {
while (node._next && node._next.type === 'text') {
const next = node._next;
node.literal += next.literal;
node._next = next._next;
if (node._next) {
node._next._prev = node;
}
if (node._parent._lastChild === next) {
node._parent._lastChild = node;
}
}
walker.resumeAt(node, false);
}
}
return ast;
}

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply';
const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, useMarkdown, showAttachment, getCustomEmoji, theme
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme
}) => {
if (!attachments || attachments.length === 0) {
return null;
@ -16,17 +16,17 @@ 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} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />;
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} 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} theme={theme} />;
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
});
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
@ -35,7 +35,6 @@ Attachments.propTypes = {
timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
useMarkdown: PropTypes.bool,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -74,7 +74,6 @@ class Audio extends React.Component {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
useMarkdown: PropTypes.bool,
theme: PropTypes.string,
split: PropTypes.bool,
getCustomEmoji: PropTypes.func
@ -157,7 +156,7 @@ class Audio extends React.Component {
uri, paused, currentTime, duration
} = this.state;
const {
user, baseUrl, file, getCustomEmoji, useMarkdown, split, theme
user, baseUrl, file, getCustomEmoji, split, theme
} = this.props;
const { description } = file;
@ -182,6 +181,7 @@ class Audio extends React.Component {
onEnd={this.onEnd}
paused={paused}
repeat={false}
ignoreSilentSwitch='ignore'
/>
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Slider
@ -199,7 +199,7 @@ class Audio extends React.Component {
/>
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
</View>
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</>
);
}

View File

@ -21,6 +21,7 @@ const Broadcast = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)}
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
hitSlop={BUTTON_HIT_SLOP}
testID='message-broadcast-reply'
>
<>
<CustomIcon name='back' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
import I18n from '../../i18n';
import styles from './styles';
@ -10,7 +11,14 @@ import { themes } from '../../constants/colors';
const Content = React.memo((props) => {
if (props.isInfo) {
return <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{getInfoMessage({ ...props })}</Text>;
const infoMessage = getInfoMessage({ ...props });
return (
<Text
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}
accessibilityLabel={infoMessage}
>{infoMessage}
</Text>
);
}
let content = null;
@ -29,9 +37,9 @@ const Content = React.memo((props) => {
preview={props.tmid && !props.isThreadRoom}
channels={props.channels}
mentions={props.mentions}
useMarkdown={props.useMarkdown && (!props.tmid || props.isThreadRoom)}
navToRoomInfo={props.navToRoomInfo}
tmid={props.tmid}
useRealName={props.useRealName}
theme={props.theme}
/>
);
@ -42,7 +50,24 @@ const Content = React.memo((props) => {
{content}
</View>
);
}, (prevProps, nextProps) => prevProps.isTemp === nextProps.isTemp && prevProps.msg === nextProps.msg && prevProps.theme === nextProps.theme);
}, (prevProps, nextProps) => {
if (prevProps.isTemp !== nextProps.isTemp) {
return false;
}
if (prevProps.msg !== nextProps.msg) {
return false;
}
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (!equal(prevProps.mentions, nextProps.mentions)) {
return false;
}
if (!equal(prevProps.channels, nextProps.channels)) {
return false;
}
return true;
});
Content.propTypes = {
isTemp: PropTypes.bool,
@ -52,13 +77,13 @@ Content.propTypes = {
msg: PropTypes.string,
theme: PropTypes.string,
isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
navToRoomInfo: PropTypes.func
navToRoomInfo: PropTypes.func,
useRealName: PropTypes.bool
};
Content.displayName = 'MessageContent';

View File

@ -41,7 +41,7 @@ export const MessageImage = React.memo(({ img, theme }) => (
));
const ImageContainer = React.memo(({
file, imageUrl, baseUrl, user, useMarkdown, showAttachment, getCustomEmoji, split, theme
file, imageUrl, baseUrl, user, showAttachment, getCustomEmoji, split, theme
}) => {
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) {
@ -55,7 +55,7 @@ const ImageContainer = React.memo(({
<Button split={split} theme={theme} onPress={onPress}>
<View>
<MessageImage img={img} theme={theme} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</View>
</Button>
);
@ -73,7 +73,6 @@ ImageContainer.propTypes = {
imageUrl: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
showAttachment: PropTypes.func,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,

View File

@ -79,7 +79,7 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
});
const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, useMarkdown, theme
attachment, baseUrl, user, getCustomEmoji, theme
}) => {
const text = attachment.text || attachment.title;
if (!text) {
@ -91,7 +91,6 @@ const Description = React.memo(({
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
useMarkdown={useMarkdown}
theme={theme}
/>
);
@ -125,7 +124,7 @@ const Fields = React.memo(({ attachment, theme }) => {
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown, split, theme
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme
}) => {
if (!attachment) {
return null;
@ -164,7 +163,6 @@ const Reply = React.memo(({
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji}
useMarkdown={useMarkdown}
theme={theme}
/>
<Fields attachment={attachment} theme={theme} />
@ -179,7 +177,6 @@ Reply.propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number,
useMarkdown: PropTypes.bool,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,
split: PropTypes.bool
@ -197,7 +194,6 @@ Description.propTypes = {
attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};

View File

@ -27,7 +27,7 @@ const styles = StyleSheet.create({
});
const Video = React.memo(({
file, baseUrl, user, useMarkdown, showAttachment, getCustomEmoji, theme
file, baseUrl, user, showAttachment, getCustomEmoji, theme
}) => {
if (!baseUrl) {
return null;
@ -54,7 +54,7 @@ const Video = React.memo(({
color={themes[theme].buttonText}
/>
</Touchable>
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</>
);
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme);
@ -63,7 +63,6 @@ Video.propTypes = {
file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -28,7 +28,6 @@ class MessageContainer extends React.Component {
isReadReceiptEnabled: PropTypes.bool,
isThreadRoom: PropTypes.bool,
useRealName: PropTypes.bool,
useMarkdown: PropTypes.bool,
autoTranslateRoom: PropTypes.bool,
autoTranslateLanguage: PropTypes.string,
status: PropTypes.number,
@ -227,7 +226,7 @@ class MessageContainer extends React.Component {
render() {
const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, 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, blocks, autoTranslate: autoTranslateMessage
@ -272,7 +271,6 @@ class MessageContainer extends React.Component {
tcount={tcount}
tlm={tlm}
tmsg={tmsg}
useMarkdown={useMarkdown}
fetchThreadName={fetchThreadName}
mentions={mentions}
channels={channels}

View File

@ -12,6 +12,7 @@ import zhCN from './locales/zh-CN';
import ptPT from './locales/pt-PT';
import esES from './locales/es-ES';
import it from './locales/it';
import ja from './locales/ja';
i18n.translations = {
en,
@ -23,7 +24,8 @@ i18n.translations = {
'pt-PT': ptPT,
'es-ES': esES,
nl,
it
it,
ja
};
i18n.fallbacks = true;

View File

@ -112,6 +112,7 @@ export default {
Back: 'Zurück',
Black: 'Schwarz',
Block_user: 'Benutzer blockieren',
Browser: 'Browser',
Broadcast_channel_Description: 'Nur autorisierte Benutzer können neue Nachrichten schreiben, die anderen Benutzer können jedoch antworten',
Broadcast_Channel: 'Broadcastkanal',
Busy: 'Beschäftigt',
@ -129,9 +130,11 @@ export default {
Click_to_join: 'Klicken um teilzunehmen!',
Close: 'Schließen',
Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl',
Change_language_loading: 'Ändere Sprache.',
Choose: 'Wählen',
Choose_from_library: 'Aus der Bibliothek auswählen',
Choose_file: 'Datei auswählen',
Choose_where_you_want_links_be_opened: 'Entscheide, wie Links geöffnet werden sollen',
Code: 'Code',
Collaborative: 'Kollaborativ',
Confirm: 'Bestätigen',
@ -148,6 +151,7 @@ export default {
Permalink: 'Permalink',
Certificate_password: 'Zertifikats-Passwort',
Clear_cache: 'Lokalen Server-Cache leeren',
Clear_cache_loading: 'Leere Cache.',
Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
Create_account: 'Ein Konto erstellen',
Create_Channel: 'Kanal erstellen',
@ -157,10 +161,12 @@ export default {
Dark: 'Dunkel',
Dark_level: 'Dunkelstufe',
Default: 'Standard',
Default_browser: 'Standard-Browser',
Delete_Room_Warning: 'Durch das Löschen eines Raums werden alle Nachrichten gelöscht, die im Raum gepostet wurden. Das kann nicht rückgängig gemacht werden.',
delete: 'löschen',
Delete: 'Löschen',
DELETE: 'LÖSCHEN',
deleting_room: 'lösche Raum',
description: 'Beschreibung',
Description: 'Beschreibung',
DESKTOP_OPTIONS: 'Desktop-Einstellungen',
@ -180,10 +186,8 @@ export default {
EMAIL: 'EMAIL',
email: 'Email',
Enable_Auto_Translate: 'Automatische Übersetzung aktivieren',
Enable_markdown: 'Markdown aktivieren',
Enable_notifications: 'Benachrichtigungen aktivieren',
Everyone_can_access_this_channel: 'Jeder kann auf diesen Kanal zugreifen',
erasing_room: 'lösche Raum',
Error_uploading: 'Fehler beim Hochladen',
Expiration_Days: 'läuft ab (Tage)',
Favorite: 'Favorisieren',
@ -206,6 +210,7 @@ export default {
Has_joined_the_channel: 'Ist dem Kanal beigetreten',
Has_joined_the_conversation: 'Hat sich dem Gespräch angeschlossen',
Has_left_the_channel: 'Hat den Kanal verlassen',
In_app: 'In-App-Browser',
IN_APP_AND_DESKTOP: 'IN-APP UND DESKTOP',
In_App_and_Desktop_Alert_info: 'Zeigt ein Banner oben am Bildschirm, wenn die App geöffnet ist und eine Benachrichtigung auf dem Desktop.',
Invisible: 'Unsichtbar',
@ -232,6 +237,7 @@ export default {
Login: 'Anmeldung',
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
Login_with: 'Einloggen mit',
Logging_out: 'Abmelden.',
Logout: 'Abmelden',
Max_number_of_uses: 'Maximale Anzahl der Benutzungen',
members: 'Mitglieder',
@ -269,9 +275,6 @@ export default {
No_results_found: 'Keine Ergebnisse gefunden',
No_starred_messages: 'Keine markierten Nachrichten',
No_thread_messages: 'Keine Threadnachrichten',
No_announcement_provided: 'Keine Ankündigung erfolgt.',
No_description_provided: 'Keine Beschreibung angegeben.',
No_topic_provided: 'Kein Thema bereitgestellt',
No_Message: 'Keine Nachricht',
No_messages_yet: 'Noch keine Nachrichten',
No_Reactions: 'Keine Reaktionen',
@ -298,6 +301,7 @@ export default {
pinned: 'angeheftet',
Pinned: 'Angeheftet',
Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein',
Please_wait: 'Bitte warten.',
Preferences: 'Einstellungen',
Preferences_saved: 'Einstellungen gespeichert!',
Privacy_Policy: ' Datenschutzbestimmungen',
@ -450,6 +454,8 @@ export default {
Username: 'Benutzername',
Username_or_email: 'Benutzername oder E-Mail-Adresse',
Validating: 'Validierung',
Verify_email_title: 'Registrierung erfolgreich!',
Verify_email_desc: 'Wir haben dir eine Email geschickt um deine Anmeldung zu bestätigen. Wenn du keine Email erhältst, komme bitte wieder und versuche es noch einmal.',
Video_call: 'Videoanruf',
View_Original: 'Original anzeigen',
Voice_call: 'Sprachanruf',
@ -496,5 +502,6 @@ export default {
New_line: 'Zeilenumbruch',
You_will_be_logged_out_of_this_application: 'Du wirst in dieser Anwendung vom Server abgemeldet.',
Clear: 'Löschen',
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten löschen.'
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten löschen.',
Mark_unread: 'Als ungelesen markieren'
};

View File

@ -112,6 +112,7 @@ export default {
Back: 'Back',
Black: 'Black',
Block_user: 'Block user',
Browser: 'Browser',
Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply',
Broadcast_Channel: 'Broadcast Channel',
Busy: 'Busy',
@ -129,9 +130,11 @@ export default {
Click_to_join: 'Click to Join!',
Close: 'Close',
Close_emoji_selector: 'Close emoji selector',
Change_language_loading: 'Changing language.',
Choose: 'Choose',
Choose_from_library: 'Choose from library',
Choose_file: 'Choose file',
Choose_where_you_want_links_be_opened: 'Choose where you want links be opened',
Code: 'Code',
Collaborative: 'Collaborative',
Confirm: 'Confirm',
@ -148,6 +151,7 @@ export default {
Permalink: 'Permalink',
Certificate_password: 'Certificate Password',
Clear_cache: 'Clear local server cache',
Clear_cache_loading: 'Clearing cache.',
Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?',
Create_account: 'Create an account',
Create_Channel: 'Create Channel',
@ -157,10 +161,12 @@ export default {
Dark: 'Dark',
Dark_level: 'Dark Level',
Default: 'Default',
Default_browser: 'Default browser',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
delete: 'delete',
Delete: 'Delete',
DELETE: 'DELETE',
deleting_room: 'deleting room',
description: 'description',
Description: 'Description',
DESKTOP_OPTIONS: 'DESKTOP OPTIONS',
@ -180,10 +186,8 @@ export default {
EMAIL: 'EMAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_markdown: 'Enable markdown',
Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
Expiration_Days: 'Expiration (Days)',
Favorite: 'Favorite',
@ -206,6 +210,22 @@ export default {
Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel',
Hide_System_Messages: 'Hide System Messages',
Hide_type_messages: 'Hide "{{type}}" messages',
Message_HideType_uj: 'User Join',
Message_HideType_ul: 'User Leave',
Message_HideType_ru: 'User Removed',
Message_HideType_au: 'User Added',
Message_HideType_mute_unmute: 'User Muted / Unmuted',
Message_HideType_r: 'Room Name Changed',
Message_HideType_ut: 'User Joined Conversation',
Message_HideType_wm: 'Welcome',
Message_HideType_rm: 'Message Removed',
Message_HideType_subscription_role_added: 'Was Set Role',
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
Message_HideType_room_archived: 'Room Archived',
Message_HideType_room_unarchived: 'Room Unarchived',
In_app: 'In-app',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
Invisible: 'Invisible',
@ -232,6 +252,7 @@ export default {
Login: 'Login',
Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with',
Logging_out: 'Logging out.',
Logout: 'Logout',
Max_number_of_uses: 'Max number of uses',
members: 'members',
@ -269,9 +290,7 @@ export default {
No_results_found: 'No results found',
No_starred_messages: 'No starred messages',
No_thread_messages: 'No thread messages',
No_announcement_provided: 'No announcement provided.',
No_description_provided: 'No description provided.',
No_topic_provided: 'No topic provided.',
No_label_provided: 'No {{label}} provided.',
No_Message: 'No Message',
No_messages_yet: 'No messages yet',
No_Reactions: 'No Reactions',
@ -291,6 +310,7 @@ export default {
Only_authorized_users_can_write_new_messages: 'Only authorized users can write new messages',
Open_emoji_selector: 'Open emoji selector',
Open_Source_Communication: 'Open Source Communication',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password',
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
Pin: 'Pin',
@ -298,6 +318,7 @@ export default {
pinned: 'pinned',
Pinned: 'Pinned',
Please_enter_your_password: 'Please enter your password',
Please_wait: 'Please wait.',
Preferences: 'Preferences',
Preferences_saved: 'Preferences saved!',
Privacy_Policy: ' Privacy Policy',
@ -449,6 +470,7 @@ export default {
Username_is_empty: 'Username is empty',
Username: 'Username',
Username_or_email: 'Username or email',
Uses_server_configuration: 'Uses server configuration',
Validating: 'Validating',
Verify_email_title: 'Registration Succeeded!',
Verify_email_desc: 'We have sent you an email to confirm your registration. If you do not receive an email shortly, please come back and try again.',
@ -469,6 +491,7 @@ export default {
You_can_search_using_RegExp_eg: 'You can use RegExp. e.g. `/^text$/i`',
You_colon: 'You: ',
you_were_mentioned: 'you were mentioned',
You_were_removed_from_channel: 'You were removed from {{channel}}',
you: 'you',
You: 'You',
Logged_out_by_server: 'You\'ve been logged out by the server. Please log in again.',
@ -498,5 +521,6 @@ export default {
New_line: 'New line',
You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
Clear: 'Clear',
This_will_clear_all_your_offline_data: 'This will clear all your offline data.'
This_will_clear_all_your_offline_data: 'This will clear all your offline data.',
Mark_unread: 'Mark Unread'
};

View File

@ -158,6 +158,7 @@ export default {
delete: 'eliminar',
Delete: 'Eliminar',
DELETE: 'ELIMINAR',
deleting_room: 'eliminando sala',
description: 'descripción',
Description: 'Descripción',
DESKTOP_OPTIONS: 'OPCIONES DE ESCRITORIO',
@ -176,10 +177,8 @@ export default {
EMAIL: 'EMAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Permitir Auto-Translate',
Enable_markdown: 'Permitir markdown',
Enable_notifications: 'Permitir notificaciones',
Everyone_can_access_this_channel: 'Todos los usuarios pueden acceder a este canal',
erasing_room: 'eliminando sala',
Error_uploading: 'Error en la subida',
Favorite: 'Favorito',
Favorites: 'Favoritos',
@ -256,9 +255,6 @@ export default {
No_results_found: 'No hay resultados',
No_starred_messages: 'No hay mensajes destacados',
No_thread_messages: 'No hay hilots',
No_announcement_provided: 'No se ha indicado un anuncio',
No_description_provided: 'No se ha indicado descripción',
No_topic_provided: 'No se ha indicado asunto.',
No_Message: 'Sin mensajes',
No_messages_yet: 'No hay todavía mensajes',
No_Reactions: 'No hay reacciones',

View File

@ -138,6 +138,7 @@ export default {
delete: 'supprimez',
Delete: 'Supprimez',
DELETE: 'SUPPRIMEZ',
deleting_room: 'effacement de la salle',
description: 'la description',
Description: 'La description',
Disable_notifications: 'Désactiver les notifications',
@ -145,7 +146,6 @@ export default {
Dont_Have_An_Account: 'Vous n\'avez pas de compte?',
Do_you_really_want_to_key_this_room_question_mark: 'Voulez-vous vraiment {{key}} cette salle?',
edit: 'modifier',
erasing_room: 'effacement de la salle',
Edit: 'Modifier',
Email_or_password_field_is_empty: 'Le champ e-mail ou mot de passe est vide',
Email: 'E-mail',
@ -215,9 +215,6 @@ export default {
No_pinned_messages: 'Aucun message épinglé',
No_results_found: 'Aucun résultat trouvé',
No_starred_messages: 'Pas de messages suivis',
No_announcement_provided: 'Aucune annonce fournie.',
No_description_provided: 'Aucune description fournie.',
No_topic_provided: 'Aucun sujet fourni.',
No_Message: 'Aucun message',
No_Reactions: 'Aucune réaction',
Not_logged: 'Non connecté',

View File

@ -160,6 +160,7 @@ export default {
delete: 'elimina',
Delete: 'Elimina',
DELETE: 'ELIMINA',
deleting_room: 'cancellazione stanza',
description: 'descrizione',
Description: 'Descrizione',
DESKTOP_OPTIONS: 'OPZIONI DESKTOP',
@ -179,10 +180,8 @@ export default {
EMAIL: 'E-MAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Abilita traduzione automatica',
Enable_markdown: 'Abilita Markdown',
Enable_notifications: 'Abilita notifiche',
Everyone_can_access_this_channel: 'Tutti hanno accesso a questo canale',
erasing_room: 'cancellazione stanza',
Error_uploading: 'Errore nel caricamento di',
Expiration_Days: 'Scadenza (giorni)',
Favorite: 'Preferito',
@ -267,9 +266,6 @@ export default {
No_results_found: 'Nessun risultato',
No_starred_messages: 'Nessun messaggio preferito',
No_thread_messages: 'Nessun messaggio thread',
No_announcement_provided: 'Nessun annuncio inserito.',
No_description_provided: 'Nessuna descrizione inserita.',
No_topic_provided: 'Nessun argomento inserito.',
No_Message: 'Nessun messaggio',
No_messages_yet: 'Non ci sono ancora messaggi',
No_Reactions: 'Nessuna reazione',

542
app/i18n/locales/ja.js Normal file
View File

@ -0,0 +1,542 @@
export default {
'1_person_reacted': '1人がリアクション',
'1_user': '1人',
'error-action-not-allowed': '{{action}}の権限がありません。',
'error-application-not-found': 'アプリケーションがありません。',
'error-archived-duplicate-name':
'アーカイブ名が重複しています: {{room_name}}',
'error-avatar-invalid-url': '画像のURLが正しくありません: {{url}}',
'error-avatar-url-handling':
'アバターをURL({{url}})から{{username}}に設定中にエラーが発生しました。',
'error-cant-invite-for-direct-room': 'ユーザーを直接ルームに招待することができません。',
'error-could-not-change-email': 'メールアドレスを変更できません。',
'error-could-not-change-name': '名前を変更できません。',
'error-could-not-change-username': 'ユーザー名を変更できません。',
'error-delete-protected-role': '保護されたロールは削除できません。',
'error-department-not-found': 'ロールが存在しません。',
'error-direct-message-file-upload-not-allowed':
'ダイレクトメッセージでのファイルのアップロードは許可されていません。',
'error-duplicate-channel-name': '{{channel_name}}と同名のチャンネルが存在します。',
'error-email-domain-blacklisted': 'このドメインのメールアドレスはブラックリストに登録されています。',
'error-email-send-failed': '次のメールアドレスの送信に失敗しました: {{message}}',
'error-save-image': '画像の保存に失敗しました。',
'error-field-unavailable': '{{field}}は既に使用されています。',
'error-file-too-large': 'ファイルが大きすぎます。',
'error-importer-not-defined':
'インポータが正しく定義されていません。Importクラスが見つかりません。',
'error-input-is-not-a-valid-field': '{{input}}は{{field}}の入力として正しくありません。',
'error-invalid-actionlink': 'アクションリンクが正しくありません。',
'error-invalid-arguments': '引数が正しくありません。',
'error-invalid-asset': 'アセットが不正です。',
'error-invalid-channel': 'チャンネル名が不正です。',
'error-invalid-channel-start-with-chars':
'不正なチャンネルです。チャンネル名は@か#から開始します。',
'error-invalid-custom-field': 'カスタムフィールドが不正です。',
'error-invalid-custom-field-name':
'カスタムフィールド名が不正です。半角英数字、ハイフン、アンダースコアのみを使用してください。',
'error-invalid-date': '不正な日時です',
'error-invalid-description': '不正な詳細です',
'error-invalid-domain': '不正なドメインです',
'error-invalid-email': '不正なメールアドレスです。 {{emai}}',
'error-invalid-email-address': '不正なメールアドレスです',
'error-invalid-file-height': 'ファイルの高さが不正です',
'error-invalid-file-type': 'ファイルの種類が不正です',
'error-invalid-file-width': 'ファイルの幅が不正です',
'error-invalid-from-address': '不正なアドレスから通知しました',
'error-invalid-integration': '不正なインテグレーションです。',
'error-invalid-message': '不正なメッセージです。',
'error-invalid-method': '不正なメソッドです。',
'error-invalid-name': '不正な名前です',
'error-invalid-password': '不正なパスワードです',
'error-invalid-redirectUri': '不正なリダイレクトURIです',
'error-invalid-role': '不正なロールです',
'error-invalid-room': '不正なルームです',
'error-invalid-room-name': '{{room_name}}は正しいルーム名ではありません。',
'error-invalid-room-type': '{{type}}は正しいルームタイプではありません。',
'error-invalid-settings': '不正な設定が送信されました',
'error-invalid-subscription': '不正な購読です',
'error-invalid-token': 'トークンが正しくありません',
'error-invalid-triggerWords': 'トリガーワードが正しくありません',
'error-invalid-urls': 'URLが正しくありません',
'error-invalid-user': 'ユーザーが正しくありません',
'error-invalid-username': 'ユーザー名が正しくありません',
'error-invalid-webhook-response':
'WebhookのURLが200以外のステータスを返しています',
'error-message-deleting-blocked': 'メッセージの削除をブロックされています。',
'error-message-editing-blocked': 'メッセージの編集をブロックされています。',
'error-message-size-exceeded': 'メッセージの大きさが Message_MaxAllowedSize を超えています。',
'error-missing-unsubscribe-link': '購読停止リンクを入れてください。',
'error-no-tokens-for-this-user': 'このユーザーにはトークンがありません。',
'error-not-allowed': '許可されていません。',
'error-not-authorized': '有効化されていません。',
'error-push-disabled': 'プッシュは無効化されています。',
'error-remove-last-owner':
'ルームの最後のオーナーです。ルームを退出する前に新しいオーナーを設定してください。',
'error-role-in-use': '使用中のロールを削除することはできません。',
'error-role-name-required': 'ロール名を入力してください。',
'error-the-field-is-required': '{{field}}の入力欄は必須です。',
'error-too-many-requests':
'エラーです。リクエストが多すぎます。リクエストの頻度を落としてください。{{seconds}} 秒以上待ってから再度お試しください。',
'error-user-is-not-activated': 'アカウントが有効化されていません。',
'error-user-has-no-roles': 'ロールがありません。',
'error-user-limit-exceeded':
'#channel_name に招待できるユーザー数の上限を超えています。管理者にお問い合わせください。',
'error-user-not-in-room': 'ユーザーがルームにいません。',
'error-user-registration-custom-field':
'error-user-registration-custom-field',
'error-user-registration-disabled': 'ユーザー登録は無効化されています',
'error-user-registration-secret':
'ユーザーの登録は登録用URLからのみ許可されています',
'error-you-are-last-owner':
'あなたは最後のオーナーです。ルームを退出する前に別のオーナーを設定してください。',
Actions: 'アクション',
activity: 'アクティビティ',
Activity: 'アクティビティ順',
Add_Reaction: 'リアクションを追加',
Add_Server: 'サーバーを追加',
Add_users: 'ユーザーを追加',
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: '証明書を適用する',
Applying_a_theme_will_change_how_the_app_looks:
'テーマを変更すると見た目が変わります',
ARCHIVE: 'アーカイブ',
archive: 'アーカイブ',
are_typing: 'が入力中',
Are_you_sure_question_mark: 'よろしいですか?',
Are_you_sure_you_want_to_leave_the_room:
'{{room}}を退出してもよろしいですか?',
Audio: '音声',
Authenticating: '認証',
Automatic: '自動',
Auto_Translate: '自動翻訳',
Avatar_changed_successfully: 'アバターを変更しました!',
Avatar_Url: 'アバターURL',
Away: '退出中',
Back: '戻る',
Black: 'ブラック',
Block_user: 'ブロックしたユーザー',
Broadcast_channel_Description:
'許可されたユーザーのみが新しいメッセージを書き込めます。他のユーザーは返信することができます',
Broadcast_Channel: '配信チャンネル',
Busy: '取り込み中',
By_proceeding_you_are_agreeing: '続行することにより、私達を承認します',
Cancel_editing: '編集をキャンセル',
Cancel_recording: '録音をキャンセル',
Cancel: 'キャンセル',
changing_avatar: 'アバターを変更',
creating_channel: 'チャンネルを作成',
creating_invite: '招待を作成',
Channel_Name: 'チャンネル名',
Channels: 'チャンネル',
Chats: 'チャット',
Call_already_ended: '通話は終了しています。',
Click_to_join: 'クリックして参加!',
Close: '閉じる',
Close_emoji_selector: '絵文字ピッカーを閉じる',
Choose: '選択',
Choose_from_library: 'ライブラリから選択',
Choose_file: 'ファイルを選択',
Code: 'コード',
Collaborative: 'コラボ',
Confirm: '承認',
Connect: '接続',
Connect_to_a_server: 'サーバーに接続',
Connected: '接続しました',
connecting_server: 'サーバーに接続中',
Connecting: '接続中...',
Contact_us: 'お問い合わせ',
Contact_your_server_admin: 'サーバー管理者にお問い合わせください。',
Continue_with: '次でログイン: ',
Copied_to_clipboard: 'クリップボードにコピー!',
Copy: 'コピー',
Permalink: 'パーマリンク',
Certificate_password: 'パスワード証明書',
Clear_cache: 'ローカルのサーバーキャッシュをクリア',
Whats_the_password_for_your_certificate:
'証明書のパスワードはなんですか?',
Create_account: 'アカウントを作成',
Create_Channel: 'チャンネルを作成',
Created_snippet: 'スニペットを作成',
Create_a_new_workspace: '新しいワークスペースを作成',
Create: '作成',
Dark: 'ダーク',
Dark_level: 'ダークレベル',
Default: 'デフォルト',
Default_browser: 'デフォルトのブラウザ',
Delete_Room_Warning:
'ルームを削除すると、ルームに投稿されたすべてのメッセージが削除されます。この操作は取り消せません。',
delete: '削除',
Delete: '削除',
DELETE: '削除',
deleting_room: 'ルームを削除',
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: '編集',
Edit_Invite: '編集に招待',
Email_or_password_field_is_empty: 'メールアドレスかパスワードの入力欄が空です',
Email: 'メールアドレス',
EMAIL: 'メールアドレス',
email: 'メールアドレス',
Enable_Auto_Translate: '自動翻訳を有効にする',
Enable_markdown: 'マークダウンを有効にする',
Enable_notifications: '通知を有効にする',
Everyone_can_access_this_channel: '全員このチャンネルにアクセスできます',
Error_uploading: 'アップロードエラー',
Expiration_Days: '期限切れ (日)',
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: 'クリックしてテーブル全体を見る',
Generate_New_Link: '新しいリンクを生成',
Group_by_favorites: 'お気に入りをグループ化',
Group_by_type: 'タイプ別にグループ化',
Hide: '隠す',
Has_joined_the_channel: 'はチャンネルに参加しました',
Has_joined_the_conversation: 'は会話に参加しました',
Has_left_the_channel: 'はチャンネルを退出しました',
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_or_expired_invite_token: '招待トークンが無効か、期限が切れています',
Invalid_server_version:
'接続しようとしているサーバーのバージョン({{currentVersion}})はサポートされていません。\n\nサポートする最低バージョンは {{minVersion}} です',
Invite_Link: '招待リンク',
Invite_users: 'ユーザーを招待',
Join_the_community: 'コミュニティに参加',
Join: '参加',
Just_invited_people_can_access_this_channel:
'招待されたユーザーだけがこのチャンネルに参加できます',
Language: '言語',
last_message: '最後のメッセージ',
Leave_channel: 'チャンネルを退出',
leaving_room: 'チャンネルを退出',
leave: '退出',
Legal: '法的項目',
Light: 'ライト',
License: 'ライセンス',
Livechat: 'ライブチャット',
Login: 'ログイン',
Login_error: '証明書が承認されませんでした。再度お試しください。',
Login_with: '次でログイン: ',
Logout: 'ログアウト',
Max_number_of_uses: '最大利用数',
members: 'メンバー',
Members: 'メンバー',
Mentioned_Messages: 'メンションされたメッセージ',
mentioned: 'メンション',
Mentions: 'メンション',
Message_accessibility: '{{user}} から {{time}} にメッセージ: {{message}}',
Message_actions: 'メッセージアクション',
Message_pinned: 'メッセージをピン留め',
Message_removed: 'メッセージを除く',
message: 'メッセージ',
messages: 'メッセージ',
Message: 'メッセージ',
Messages: 'メッセージ',
Message_Reported: 'メッセージを報告しました',
Microphone_Permission_Message:
'Rocket Chatは音声メッセージを送信するのにマイクのアクセスの許可が必要です。',
Microphone_Permission: 'マイクの許可',
Mute: 'ミュート',
muted: 'ミュートした',
My_servers: '自分のサーバー',
N_people_reacted: '{{n}}人がリアクションしました',
N_users: '{{n}}人',
name: 'アルファベット',
Name: '名前',
Never: 'ずっと受け取らない',
New_Message: 'メッセージ',
New_Password: '新しいパスワード',
New_Server: '新規サーバー',
Next: '次へ',
No_files: 'ファイルがありません',
No_limit: '制限なし',
No_mentioned_messages: 'メンションされたメッセージはありません',
No_pinned_messages: 'ピン留めされたメッセージはありません',
No_results_found: '結果なし',
No_starred_messages: 'お気に入りされたメッセージはありません',
No_thread_messages: 'スレッドのメッセージはありません',
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: '絵文字ピッカーを開く',
Open_Source_Communication: 'オープンソースコミュニケーション',
Password: 'パスワード',
Permalink_copied_to_clipboard: 'リンクをクリップボードにコピーしました!',
Pin: 'ピン留め',
Pinned_Messages: 'ピン留めされたメッセージ',
pinned: 'ピン留めされた',
Pinned: 'ピン留めされました',
Please_enter_your_password: 'パスワードを入力してください',
Preferences: '設定',
Preferences_saved: '設定が保存されました。',
Privacy_Policy: ' プライバシーポリシー',
Private_Channel: 'プライベートチャンネル',
Private_Groups: 'プライベートグループ',
Private: 'プライベート',
Processing: '処理中...',
Profile_saved_successfully: 'プロフィールが保存されました!',
Profile: 'プロフィール',
Public_Channel: 'パブリックチャンネル',
Public: 'パブリック',
PUSH_NOTIFICATIONS: 'プッシュ通知',
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: 'パスワードをリセット',
resetting_password: 'パスワードを再設定',
RESET: 'リセット',
Review_app_title: 'アプリにご満足いただけておりますか?',
Review_app_desc: '{{store}}で5段階で評価をお願いします',
Review_app_yes: 'はい!',
Review_app_no: 'いいえ',
Review_app_later: 'あとで',
Review_app_unable_store: '{{store}}を開けません。',
Review_this_app: 'アプリをレビューする',
Roles: 'ロール',
Room_actions: 'ルームアクション',
Room_changed_announcement:
'{{userBy}}がアナウンスを変更しました: {{announcement}}',
Room_changed_description:
'{{userBy}}が概要を変更しました: {{description}}',
Room_changed_privacy: '{{userBy}}がルームタイプを変更しました。: {{type}}',
Room_changed_topic: '{{userBy}}がトピックを変更しました: {{topic}}',
Room_Files: 'ルームのファイル',
Room_Info_Edit: 'ルーム情報を編集',
Room_Info: 'ルーム情報',
Room_Members: 'ルームのメンバー',
Room_name_changed: 'ルーム名が{{userBy}}により変更されました: {{name}}',
SAVE: '保存',
Save_Changes: '変更を保存',
Save: '保存',
saving_preferences: '設定を保存中',
saving_profile: 'プロフィールを設定中',
saving_settings: 'サーバー設定を保存中',
saved_to_gallery: 'ギャラリーに保存しました',
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_Link: 'リンクをシェアする',
Share_this_app: 'このアプリをシェアする',
Show_more: 'Show more..',
Show_Unread_Counter: '未読件数を表示する',
Show_Unread_Counter_Info:
'未読件数はリスト上で、チャンネルの右側にバッジで表示されます。',
Sign_in_your_server: 'サーバーに接続',
Sign_Up: '登録',
Some_field_is_invalid_or_empty: '不正、または空の入力欄があります。',
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:
'不正なURLか、セキュアな接続を確立できませんでした。\n{{contact}}',
There_was_an_error_while_action: '{{action}}の最中にエラーが発生しました!',
This_room_is_blocked: 'このルームはブロックされています。',
This_room_is_read_only: 'このルームは読み取り専用です。',
Thread: 'スレッド',
Threads: 'スレッド',
Timezone: 'タイムゾーン',
To: 'To',
topic: 'トピック',
Topic: 'トピック',
Translate: '翻訳',
Try_again: '再度お試しください。',
Two_Factor_Authentication: '2段階認証',
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: '{{userBy}} が {{userAdded}} を追加しました',
User_Info: 'ユーザー情報',
User_has_been_key: 'ユーザーは{{ key }}',
User_is_no_longer_role_by_: '{{userBy}} は {{user}} のロール {{role}} を削除しました。',
User_muted_by: '{{userBy}} は {{userMuted}} をミュートしました。',
User_removed_by: '{{userBy}} は {{userRemoved}} を退出させました。',
User_sent_an_attachment: '{{user}}は添付ファイルを送信しました',
User_unmuted_by: '{{userUnmuted}} は {{userBy}} にミュート解除されました。',
User_was_set_role_by_: '{{user}} was set {{role}} by {{userBy}}',
Username_is_empty: 'ユーザー名が空です。',
Username: 'ユーザー名',
Username_or_email: 'ユーザー名かメールアドレス',
Validating: '検証中',
Video_call: 'ビデオ通話',
View_Original: 'オリジナルを見る',
Voice_call: '音声通話',
Websocket_disabled: 'Websocketはこのサーバーでは無効化されています。\n{{contact}}',
Welcome: 'ようこそ',
Welcome_to_RocketChat: 'Rocket.Chatへようこそ',
Whats_your_2fa: '2段階認証のコードを入力してください',
Without_Servers: 'サーバーを除く',
Write_External_Permission_Message:
'Rocket Chatは画像を保存するためにギャラリーへのアクセスを求めています。',
Write_External_Permission: 'ギャラリーへのアクセス許可',
Yes_action_it: 'はい、{{action}}します!',
Yesterday: '昨日',
You_are_in_preview_mode: 'プレビューモードです。',
You_are_offline: 'オフラインです。',
You_can_search_using_RegExp_eg: '正規表現を利用できます。 例: `/^text$/i`',
You_colon: 'あなた: ',
you_were_mentioned: 'あなたがメンションしました',
you: 'あなた',
You: 'あなた',
Logged_out_by_server:
'サーバーからログアウトします。もう一度ログインしてください。',
You_need_to_access_at_least_one_RocketChat_server_to_share_something:
'シェアするためには1つ以上のサーバーにアクセスする必要があります。',
Your_certificate: 'あなたの認証情報',
Your_invite_link_will_expire_after__usesLeft__uses:
'招待リンクはあと{{usesLeft}}回で使用できなくなります。',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses:
'招待リンクは{{date}}までか、あと{{usesLeft}}回で使用できなくなります。',
Your_invite_link_will_expire_on__date__:
'招待リンクは{{date}}に使用できなくなります。',
Your_invite_link_will_never_expire: '招待リンクはずっと有効です。',
Version_no: 'バージョン: {{version}}',
You_will_not_be_able_to_recover_this_message:
'このメッセージは復元できません!',
Change_Language: '言語を変更',
Crash_report_disclaimer:
'クラッシュレポートには問題を特定し、修正するために必要な情報のみが含まれます。チャット内のコンテンツは送信されません。',
Type_message: 'メッセージを入力',
Room_search: 'ルームを検索',
Room_selection: 'ルームを選択 1...9',
Next_room: '次のルーム',
Previous_room: '前のルーム',
New_room: '新しいルーム',
Upload_room: 'ルームにアップロード',
Search_messages: 'メッセージを検索',
Scroll_messages: 'メッセージをスクロール',
Reply_latest: '最新のメッセージにリプライ',
Server_selection: 'サーバー選択',
Server_selection_numbers: 'サーバー選択 1...9',
Add_server: 'サーバーを追加',
New_line: '新しい行',
You_will_be_logged_out_of_this_application:
'アプリからログアウトします。',
Clear: 'クリア',
This_will_clear_all_your_offline_data:
'オフラインデータをすべて削除します。'
};

View File

@ -160,6 +160,7 @@ export default {
delete: 'delete',
Delete: 'Delete',
DELETE: 'DELETE',
deleting_room: 'kamer legen',
description: 'beschrijving',
Description: 'Beschrijving',
DESKTOP_OPTIONS: 'DESKTOP OPTIES',
@ -179,10 +180,8 @@ export default {
EMAIL: 'EMAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Zet Auto-Translate aan',
Enable_markdown: 'Zet markdown aan',
Enable_notifications: 'Zet notifications aan',
Everyone_can_access_this_channel: 'Iedereen kan bij dit kanaal',
erasing_room: 'kamer legen',
Error_uploading: 'Error tijdens uploaden',
Expiration_Days: 'Vervalt in (Dagen)',
Favorite: 'Favoriet',
@ -267,9 +266,6 @@ export default {
No_results_found: 'Geen resultaten gevonden',
No_starred_messages: 'Geen berichten met ster gemarkeerd',
No_thread_messages: 'Geen thread berichten',
No_announcement_provided: 'Geen announcement opgegeven.',
No_description_provided: 'Geen beschrijving opgegeven.',
No_topic_provided: 'Geen onderwerp opgegeven.',
No_Message: 'Geen bericht',
No_messages_yet: 'Nog geen berichten',
No_Reactions: 'Geen reacties',

View File

@ -114,6 +114,7 @@ export default {
Back: 'Voltar',
Black: 'Preto',
Block_user: 'Bloquear usuário',
Browser: 'Navegador',
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',
Busy: 'Ocupado',
@ -127,13 +128,16 @@ export default {
Channel_Name: 'Nome do Canal',
Channels: 'Canais',
Chats: 'Conversas',
Change_language_loading: 'Alterando idioma.',
Call_already_ended: 'A chamada já terminou!',
Clear_cache_loading: 'Limpando cache.',
Click_to_join: 'Clique para participar!',
Close: 'Fechar',
Close_emoji_selector: 'Fechar seletor de emojis',
Choose: 'Escolher',
Choose_from_library: 'Escolha da biblioteca',
Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
Code: 'Código',
Collaborative: 'Colaborativo',
Confirm: 'Confirmar',
@ -154,10 +158,12 @@ export default {
Create: 'Criar',
Dark: 'Escuro',
Dark_level: 'Nível escuro',
Default_browser: 'Navegador padrão',
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',
DELETE: 'EXCLUIR',
deleting_room: 'excluindo sala',
Direct_Messages: 'Mensagens Diretas',
Directory: 'Diretório',
description: 'descrição',
@ -168,13 +174,11 @@ export default {
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
edit: 'editar',
edited: 'editado',
erasing_room: 'apagando sala',
Edit: 'Editar',
Edit_Invite: 'Editar convite',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
Enable_markdown: 'Habilitar markdown',
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',
@ -197,6 +201,22 @@ export default {
Has_joined_the_channel: 'Entrou no canal',
Has_joined_the_conversation: 'Entrou na conversa',
Has_left_the_channel: 'Saiu da conversa',
Hide_System_Messages: 'Esconder mensagens do sistema',
Hide_type_messages: 'Esconder mensagens de "{{type}}"',
Message_HideType_uj: 'Utilizador Entrou',
Message_HideType_ul: 'Utilizador Saiu',
Message_HideType_ru: 'Utilizador Removido',
Message_HideType_au: 'Utilizador adicionado',
Message_HideType_mute_unmute: 'Utilizador Silenciado',
Message_HideType_r: 'Nome da sala alterado',
Message_HideType_ut: 'Utilizador adicionado ao bate-papo',
Message_HideType_wm: 'Bem Vindo',
Message_HideType_rm: 'Mensagem Removida',
Message_HideType_subscription_role_added: 'Papel atribuído',
Message_HideType_subscription_role_removed: 'Papel removido',
Message_HideType_room_archived: 'Sala arquivada',
Message_HideType_room_unarchived: 'Sala desarquivada',
In_app: 'No app',
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
@ -219,6 +239,7 @@ export default {
Login_error: 'Suas credenciais foram rejeitadas. Tente novamente por favor!',
Login_with: 'Login with',
Logout: 'Sair',
Logging_out: 'Saindo.',
Max_number_of_uses: 'Número máximo de usos',
Members: 'Membros',
Mentioned_Messages: 'Mensagens mencionadas',
@ -251,9 +272,7 @@ export default {
No_results_found: 'Nenhum resultado encontrado',
No_starred_messages: 'Não há mensagens favoritas',
No_thread_messages: 'Não há tópicos',
No_announcement_provided: 'Sem anúncio.',
No_description_provided: 'Sem descrição.',
No_topic_provided: 'Sem tópico.',
No_label_provided: 'Sem {{label}}.',
No_Message: 'Não há mensagens',
No_messages_yet: 'Não há mensagens ainda',
No_Reactions: 'Sem reações',
@ -267,12 +286,14 @@ export default {
Only_authorized_users_can_write_new_messages: 'Somente usuários autorizados podem escrever novas mensagens',
Open_emoji_selector: 'Abrir seletor de emoji',
Open_Source_Communication: 'Comunicação Open Source',
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha',
Permalink_copied_to_clipboard: 'Link-permanente copiado para a área de transferência!',
Pin: 'Fixar',
Pinned_Messages: 'Mensagens Fixadas',
pinned: 'fixada',
Pinned: 'Mensagens Fixadas',
Please_wait: 'Por favor, aguarde.',
Please_enter_your_password: 'Por favor, digite sua senha',
Preferences: 'Preferências',
Preferences_saved: 'Preferências salvas!',
@ -404,6 +425,7 @@ export default {
Username_is_empty: 'Usuário está vazio',
Username: 'Usuário',
Username_or_email: 'Usuário ou email',
Uses_server_configuration: 'Usar configuração do servidor',
Verify_email_title: 'Registrado com sucesso!',
Verify_email_desc: 'Nós lhe enviamos um e-mail para confirmar o seu registro. Se você não receber um e-mail em breve, por favor retorne e tente novamente.',
Video_call: 'Chamada de vídeo',
@ -420,6 +442,7 @@ export default {
You_can_search_using_RegExp_eg: 'Você pode usar expressões regulares, por exemplo `/^text$/i`',
You_colon: 'Você: ',
you_were_mentioned: 'você foi mencionado',
You_were_removed_from_channel: 'Você foi removido de {{channel}}',
you: 'você',
You: 'Você',
Your_invite_link_will_expire_after__usesLeft__uses: 'Seu link de convite irá vencer depois de {{usesLeft}} usos.',
@ -446,5 +469,6 @@ export default {
New_line: 'Nova linha',
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
Clear: 'Limpar',
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.'
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.',
Mark_unread: 'Marcar como não Lida'
};

View File

@ -145,7 +145,7 @@ export default {
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer mesmo {{key}} esta sala?',
edit: 'editar',
erasing_room: 'apagando sala',
deleting_room: 'apagando sala',
Edit: 'Editar',
Email_or_password_field_is_empty: 'O campo de e-mail ou palavra-passe está vazio',
Email: 'E-mail',
@ -216,9 +216,6 @@ export default {
No_pinned_messages: 'Nenhuma mensagem afixada',
No_results_found: 'Nenhum resultado encontrado',
No_starred_messages: 'Nenhuma mensagem marcada com estrela',
No_announcement_provided: 'Nenhum anúncio fornecido.',
No_description_provided: 'Nenhuma descrição fornecida.',
No_topic_provided: 'Nenhum tópico fornecido.',
No_Message: 'Nenhuma mensagem',
No_Reactions: 'Nenhuma reação',
Not_logged: 'Não ligado',

View File

@ -171,10 +171,9 @@ export default {
EMAIL: 'EMAIL',
email: 'e-mail',
Enable_Auto_Translate: 'Включить автоперевод',
Enable_markdown: 'Включить markdown',
Enable_notifications: 'Включить уведомления',
Everyone_can_access_this_channel: 'Каждый может получить доступ к этому каналу',
erasing_room: 'стирание комнаты',
deleting_room: 'стирание комнаты',
Error_uploading: 'Ошибка при загрузке',
Favorite: 'Избранное',
Favorites: 'Избранные',
@ -250,9 +249,6 @@ export default {
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: 'Нет реакций',

View File

@ -144,7 +144,7 @@ export default {
Dont_Have_An_Account: '还没有账号?',
Do_you_really_want_to_key_this_room_question_mark: '你真的想要{{key}}这个房间吗?',
edit: '编辑',
erasing_room: '正抹去房间',
deleting_room: '正抹去房间',
Edit: '编辑',
Email_or_password_field_is_empty: '邮件或密码字段为空',
Email: '邮箱',
@ -212,9 +212,6 @@ export default {
No_pinned_messages: '没有固定的消息',
No_snippeted_messages: '没有代码片段的消息',
No_starred_messages: '没有加星标的消息',
No_announcement_provided: '没有公告.',
No_description_provided: '没有描述.',
No_topic_provided: '没有话题.',
No_Message: '没有消息',
No_Reactions: '没有回复',
Not_logged: '没有记录',

View File

@ -217,6 +217,9 @@ const SettingsStack = createStackNavigator({
},
ThemeView: {
getScreen: () => require('./views/ThemeView').default
},
DefaultBrowserView: {
getScreen: () => require('./views/DefaultBrowserView').default
}
}, {
defaultNavigationOptions: defaultHeader,
@ -466,7 +469,7 @@ class CustomModalStack extends React.Component {
closeModal();
return true;
}
if (state && state.routes[state.index] && state.routes[state.index].routes.length > 1) {
if (state && state.routes[state.index] && state.routes[state.index].routes && state.routes[state.index].routes.length > 1) {
navigation.goBack();
}
return false;
@ -602,9 +605,6 @@ export default class Root extends React.Component {
}
init = async() => {
if (isIOS) {
await RNUserDefaults.setName('group.ios.chat.rocket');
}
RNUserDefaults.objectForKey(THEME_PREFERENCES_KEY).then(this.setTheme);
const [notification, deepLinking] = await Promise.all([initializePushNotifications(), Linking.getInitialURL()]);
const parsedDeepLinkingURL = parseDeepLinking(deepLinking);

View File

@ -1,5 +1,7 @@
import { Model } from '@nozbe/watermelondb';
import { field, date } from '@nozbe/watermelondb/decorators';
import { field, date, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class Setting extends Model {
static table = 'settings';
@ -10,5 +12,7 @@ export default class Setting extends Model {
@field('value_as_number') valueAsNumber;
@json('value_as_array', sanitizer) valueAsArray;
@date('_updated_at') _updatedAt;
}

View File

@ -89,4 +89,6 @@ export default class Subscription extends Model {
@children('thread_messages') threadMessages;
@field('hide_unread_status') hideUnreadStatus;
@json('sys_mes', sanitizer) sysMes;
}

View File

@ -40,6 +40,28 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 5,
steps: [
addColumns({
table: 'settings',
columns: [
{ name: 'value_as_array', type: 'string', isOptional: true }
]
})
]
},
{
toVersion: 6,
steps: [
addColumns({
table: 'subscriptions',
columns: [
{ name: 'sys_mes', type: 'string', isOptional: true }
]
})
]
}
]
});

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 4,
version: 6,
tables: [
tableSchema({
name: 'subscriptions',
@ -39,7 +39,8 @@ export default appSchema({
{ name: 'jitsi_timeout', type: 'number', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'auto_translate_language', type: 'string' },
{ name: 'hide_unread_status', type: 'boolean', isOptional: true }
{ name: 'hide_unread_status', type: 'boolean', isOptional: true },
{ name: 'sys_mes', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -196,6 +197,7 @@ export default appSchema({
{ name: 'value_as_string', type: 'string', isOptional: true },
{ name: 'value_as_boolean', type: 'boolean', isOptional: true },
{ name: 'value_as_number', type: 'number', isOptional: true },
{ name: 'value_as_array', type: 'string', isOptional: true },
{ name: '_updated_at', type: 'number', isOptional: true }
]
}),

View File

@ -1,5 +1,6 @@
import random from '../../utils/random';
import EventEmitter from '../../utils/events';
import fetch from '../../utils/fetch';
import Navigation from '../Navigation';
const ACTION_TYPES = {

View File

@ -52,6 +52,19 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
});
};
export async function getLoginSettings({ server }) {
try {
const settingsParams = JSON.stringify(['Accounts_ShowFormLogin', 'Accounts_RegistrationForm']);
const result = await fetch(`${ server }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());
if (result.success && result.settings.length) {
reduxStore.dispatch(actions.addSettings(this.parseSettings(this._prepareSettings(result.settings))));
}
} catch (e) {
log(e);
}
}
export async function setSettings() {
const db = database.active;
const settingsCollection = db.collections.get('settings');
@ -61,9 +74,10 @@ export async function setSettings() {
valueAsString: item.valueAsString,
valueAsBoolean: item.valueAsBoolean,
valueAsNumber: item.valueAsNumber,
valueAsArray: item.valueAsArray,
_updatedAt: item._updatedAt
}));
reduxStore.dispatch(actions.setAllSettings(RocketChat.parseSettings(parsed.slice(0, parsed.length))));
reduxStore.dispatch(actions.addSettings(RocketChat.parseSettings(parsed.slice(0, parsed.length))));
}
export default async function() {

View File

@ -0,0 +1,72 @@
import { InteractionManager } from 'react-native';
import semver from 'semver';
import reduxStore from '../createStore';
import { setActiveUsers } from '../../actions/activeUsers';
export function subscribeUsersPresence() {
const serverVersion = reduxStore.getState().server.version;
// if server is lower than 1.1.0
if (serverVersion && semver.lt(semver.coerce(serverVersion), '1.1.0')) {
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
this.activeUsersSubTimeout = setTimeout(() => {
this.sdk.subscribe('activeUsers');
}, 5000);
} else {
this.sdk.subscribe('stream-notify-logged', 'user-status');
}
}
let ids = [];
export default async function getUsersPresence() {
const serverVersion = reduxStore.getState().server.version;
// if server is greather than or equal 1.1.0
if (serverVersion && !semver.lt(semver.coerce(serverVersion), '1.1.0')) {
let params = {};
// if server is greather than or equal 3.0.0
if (serverVersion && !semver.lt(semver.coerce(serverVersion), '3.0.0')) {
// if not have any id
if (!ids.length) {
return;
}
// Request userPresence on demand
params = { ids: ids.join(',') };
ids = [];
}
// RC 1.1.0
const result = await this.sdk.get('users.presence', params);
if (result.success) {
const activeUsers = result.users.reduce((ret, item) => {
ret[item._id] = item.status;
return ret;
}, {});
InteractionManager.runAfterInteractions(() => {
reduxStore.dispatch(setActiveUsers(activeUsers));
});
}
}
}
let usersTimer = null;
export function getUserPresence(uid) {
const auth = reduxStore.getState().login.isAuthenticated;
if (!usersTimer) {
usersTimer = setTimeout(() => {
if (auth && ids.length) {
getUsersPresence.call(this);
}
usersTimer = null;
}, 2000);
}
ids.push(uid);
}

View File

@ -32,6 +32,7 @@ export const merge = (subscription, room) => {
} else {
subscription.muted = [];
}
subscription.sysMes = room.sysMes;
}
if (!subscription.name) {

View File

@ -1,8 +1,8 @@
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import database from '../database';
import log from '../../utils/log';
import { headers } from '../../utils/fetch';
const uploadQueue = {};
@ -75,7 +75,10 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
xhr.setRequestHeader('X-Auth-Token', token);
xhr.setRequestHeader('X-User-Id', id);
xhr.setRequestHeader('User-Agent', headers['User-Agent']);
const { customHeaders } = RocketChatSettings;
Object.keys(customHeaders).forEach((key) => {
xhr.setRequestHeader(key, customHeaders[key]);
});
xhr.upload.onprogress = async({ total, loaded }) => {
try {

View File

@ -5,7 +5,7 @@ import database from '../database';
import log from '../../utils/log';
import random from '../../utils/random';
const changeMessageStatus = async(id, tmid, status) => {
const changeMessageStatus = async(id, tmid, status, message) => {
const db = database.active;
const msgCollection = db.collections.get('messages');
const threadMessagesCollection = db.collections.get('thread_messages');
@ -14,6 +14,10 @@ const changeMessageStatus = async(id, tmid, status) => {
successBatch.push(
messageRecord.prepareUpdate((m) => {
m.status = status;
if (message) {
m.mentions = message.mentions;
m.channels = message.channels;
}
})
);
@ -22,6 +26,10 @@ const changeMessageStatus = async(id, tmid, status) => {
successBatch.push(
threadMessageRecord.prepareUpdate((tm) => {
tm.status = status;
if (message) {
tm.mentions = message.mentions;
tm.channels = message.channels;
}
})
);
}
@ -42,15 +50,18 @@ export async function sendMessageCall(message) {
try {
const sdk = this.shareSDK || this.sdk;
// RC 0.60.0
await sdk.post('chat.sendMessage', {
const result = await sdk.post('chat.sendMessage', {
message: {
_id, rid, msg, tmid
}
});
await changeMessageStatus(_id, tmid, messagesStatus.SENT);
} catch (e) {
await changeMessageStatus(_id, tmid, messagesStatus.ERROR);
if (result.success) {
return changeMessageStatus(_id, tmid, messagesStatus.SENT, result.message);
}
} catch {
// do nothing
}
return changeMessageStatus(_id, tmid, messagesStatus.ERROR);
}
export default async function(rid, msg, tmid, user) {
@ -133,7 +144,7 @@ export default async function(rid, msg, tmid, user) {
_id: user.id || '1',
username: user.username
};
if (tmid) {
if (tmid && tMessageRecord) {
m.tmid = tmid;
m.tlm = messageDate;
m.tmsg = tMessageRecord.msg;

View File

@ -11,10 +11,17 @@ import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actio
import debounce from '../../../utils/debounce';
import RocketChat from '../../rocketchat';
const WINDOW_TIME = 1000;
export default class RoomSubscription {
constructor(rid) {
this.rid = rid;
this.isAlive = true;
this.timer = null;
this.queue = {};
this.messagesBatch = {};
this.threadsBatch = {};
this.threadMessagesBatch = {};
}
subscribe = async() => {
@ -49,6 +56,9 @@ export default class RoomSubscription {
this.removeListener(this.notifyRoomListener);
this.removeListener(this.messageReceivedListener);
reduxStore.dispatch(clearUserTyping());
if (this.timer) {
clearTimeout(this.timer);
}
}
removeListener = async(promise) => {
@ -131,15 +141,13 @@ export default class RoomSubscription {
RocketChat.readMessages(this.rid, lastOpen);
}, 300);
handleMessageReceived = protectedFunction((ddpMessage) => {
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
const lastOpen = new Date();
if (this.rid !== message.rid) {
return;
}
InteractionManager.runAfterInteractions(async() => {
updateMessage = message => (
new Promise(async(resolve) => {
if (this.rid !== message.rid) {
return;
}
const db = database.active;
const batch = [];
const msgCollection = db.collections.get('messages');
const threadsCollection = db.collections.get('threads');
const threadMessagesCollection = db.collections.get('thread_messages');
@ -154,22 +162,17 @@ export default class RoomSubscription {
// Do nothing
}
if (messageRecord) {
try {
const update = messageRecord.prepareUpdate((m) => {
Object.assign(m, message);
});
batch.push(update);
} catch (e) {
console.log(e);
}
const update = messageRecord.prepareUpdate((m) => {
Object.assign(m, message);
});
this._messagesBatch[message._id] = update;
} else {
batch.push(
msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = this.rid;
Object.assign(m, message);
}))
);
const create = msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = this.rid;
Object.assign(m, message);
}));
this._messagesBatch[message._id] = create;
}
// Create or update thread
@ -181,19 +184,17 @@ export default class RoomSubscription {
}
if (threadRecord) {
batch.push(
threadRecord.prepareUpdate(protectedFunction((t) => {
Object.assign(t, message);
}))
);
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
Object.assign(t, message);
}));
this._threadsBatch[message._id] = updateThread;
} else {
batch.push(
threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = this.rid;
Object.assign(t, message);
}))
);
const createThread = threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = this.rid;
Object.assign(t, message);
}));
this._threadsBatch[message._id] = createThread;
}
}
@ -206,35 +207,75 @@ export default class RoomSubscription {
}
if (threadMessageRecord) {
batch.push(
threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, message);
tm.rid = message.tmid;
delete tm.tmid;
}))
);
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, message);
tm.rid = message.tmid;
delete tm.tmid;
}));
this._threadMessagesBatch[message._id] = updateThreadMessage;
} else {
batch.push(
threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message);
tm.subscription.id = this.rid;
tm.rid = message.tmid;
delete tm.tmid;
}))
);
const createThreadMessage = threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message);
tm.subscription.id = this.rid;
tm.rid = message.tmid;
delete tm.tmid;
}));
this._threadMessagesBatch[message._id] = createThreadMessage;
}
}
this.read(lastOpen);
return resolve();
})
)
try {
await db.action(async() => {
await db.batch(...batch);
});
} catch (e) {
log(e);
}
});
});
handleMessageReceived = (ddpMessage) => {
if (!this.timer) {
this.timer = setTimeout(async() => {
// copy variables values to local and clean them
const _lastOpen = this.lastOpen;
const _queue = Object.keys(this.queue).map(key => this.queue[key]);
this._messagesBatch = this.messagesBatch;
this._threadsBatch = this.threadsBatch;
this._threadMessagesBatch = this.threadMessagesBatch;
this.queue = {};
this.messagesBatch = {};
this.threadsBatch = {};
this.threadMessagesBatch = {};
this.timer = null;
for (let i = 0; i < _queue.length; i += 1) {
try {
// eslint-disable-next-line no-await-in-loop
await this.updateMessage(_queue[i]);
} catch (e) {
log(e);
}
}
try {
const db = database.active;
await db.action(async() => {
await db.batch(
...Object.values(this._messagesBatch),
...Object.values(this._threadsBatch),
...Object.values(this._threadMessagesBatch)
);
});
this.read(_lastOpen);
} catch (e) {
log(e);
}
// Clean local variables
this._messagesBatch = {};
this._threadsBatch = {};
this._threadMessagesBatch = {};
}, WINDOW_TIME);
}
this.lastOpen = new Date();
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
this.queue[message._id] = message;
};
}

View File

@ -13,6 +13,8 @@ import { notificationReceived } from '../../../actions/notification';
import { handlePayloadUserInteraction } from '../actions';
import buildMessage from '../helpers/buildMessage';
import RocketChat from '../../rocketchat';
import EventEmmiter from '../../../utils/events';
import { deleteRoomFinish } from '../../../actions/room';
const removeListener = listener => listener.stop();
@ -238,6 +240,15 @@ export default function subscribeRooms() {
...threadMessagesToDelete
);
});
const roomState = store.getState().room;
// Delete and remove events come from this stream
// Here we identify which one was triggered
if (data.rid === roomState.rid && roomState.isDeleting) {
store.dispatch(deleteRoomFinish());
} else {
EventEmmiter.emit('ROOM_REMOVED', { rid: data.rid });
}
} catch (e) {
log(e);
}
@ -283,10 +294,9 @@ export default function subscribeRooms() {
const [notification] = ddpMessage.fields.args;
try {
const { payload: { rid } } = notification;
const subCollection = db.collections.get('subscriptions');
const sub = await subCollection.find(rid);
notification.title = RocketChat.getRoomTitle(sub);
notification.avatar = RocketChat.getRoomAvatar(sub);
const room = await RocketChat.getRoom(rid);
notification.title = RocketChat.getRoomTitle(room);
notification.avatar = RocketChat.getRoomAvatar(room);
} catch (e) {
// do nothing
}

View File

@ -74,17 +74,29 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
// Update
msgsToUpdate = msgsToUpdate.map((message) => {
const newMessage = update.find(m => m._id === message.id);
if (message._hasPendingUpdate) {
console.log(message);
return;
}
return message.prepareUpdate(protectedFunction((m) => {
Object.assign(m, newMessage);
}));
});
threadsToUpdate = threadsToUpdate.map((thread) => {
if (thread._hasPendingUpdate) {
console.log(thread);
return;
}
const newThread = allThreads.find(t => t._id === thread.id);
return thread.prepareUpdate(protectedFunction((t) => {
Object.assign(t, newThread);
}));
});
threadMessagesToUpdate = threadMessagesToUpdate.map((threadMessage) => {
if (threadMessage._hasPendingUpdate) {
console.log(threadMessage);
return;
}
const newThreadMessage = allThreadMessages.find(t => t._id === threadMessage.id);
return threadMessage.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, newThreadMessage);

View File

@ -1,6 +1,6 @@
import { AsyncStorage, InteractionManager } from 'react-native';
import semver from 'semver';
import { Rocketchat as RocketchatClient, settings as RocketChatSettings } from '@rocket.chat/sdk';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults';
import { Q } from '@nozbe/watermelondb';
import * as FileSystem from 'expo-file-system';
@ -12,7 +12,7 @@ import database from './database';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server';
import fetch, { headers } from '../utils/fetch';
import fetch, { BASIC_AUTH_KEY } from '../utils/fetch';
import { setUser, setLoginServices, loginRequest } from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
@ -21,10 +21,11 @@ import {
} from '../actions/share';
import subscribeRooms from './methods/subscriptions/rooms';
import getUsersPresence, { getUserPresence, subscribeUsersPresence } from './methods/getUsersPresence';
import protectedFunction from './methods/helpers/protectedFunction';
import readMessages from './methods/readMessages';
import getSettings, { setSettings } from './methods/getSettings';
import getSettings, { getLoginSettings, setSettings } from './methods/getSettings';
import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions';
@ -50,7 +51,6 @@ import I18n from '../i18n';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY';
export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY';
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
const returnAnArray = obj => obj || [];
@ -58,8 +58,6 @@ const MIN_ROCKETCHAT_VERSION = '0.70.0';
const STATUSES = ['offline', 'online', 'away', 'busy'];
RocketChatSettings.customHeaders = headers;
const RocketChat = {
TOKEN_KEY,
callJitsi,
@ -446,6 +444,7 @@ const RocketChat = {
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(TOKEN_KEY);
await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
try {
const db = database.active;
@ -621,6 +620,7 @@ const RocketChat = {
cancelUpload,
isUploadActive,
getSettings,
getLoginSettings,
setSettings,
getPermissions,
getCustomEmojis,
@ -628,7 +628,11 @@ const RocketChat = {
getSlashCommands,
getRoles,
parseSettings: settings => settings.reduce((ret, item) => {
ret[item._id] = item[defaultSettings[item._id].type];
ret[item._id] = defaultSettings[item._id] && item[defaultSettings[item._id].type];
if (item._id === 'Hide_System_Messages') {
ret[item._id] = ret[item._id]
.reduce((array, value) => [...array, ...value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]], []);
}
return ret;
}, {}),
_prepareSettings(settings) {
@ -646,6 +650,9 @@ const RocketChat = {
// RC 0.49.0
return this.sdk.post('chat.update', { roomId: rid, msgId: id, text: msg });
},
markAsUnread({ messageId }) {
return this.sdk.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
},
toggleStarMessage(messageId, starred) {
if (starred) {
// RC 0.59.0
@ -780,7 +787,7 @@ const RocketChat = {
// RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId });
},
eraseRoom(roomId, t) {
deleteRoom(roomId, t) {
// RC 0.49.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId });
},
@ -880,13 +887,6 @@ const RocketChat = {
// RC 0.51.0
return this.sdk.methodCall('setAvatarFromService', data, contentType, service);
},
async getUseMarkdown() {
const useMarkdown = await AsyncStorage.getItem(MARKDOWN_KEY);
if (useMarkdown === null) {
return true;
}
return JSON.parse(useMarkdown);
},
async getAllowCrashReport() {
const allowCrashReport = await AsyncStorage.getItem(CRASH_REPORT_KEY);
if (allowCrashReport === null) {
@ -912,7 +912,7 @@ const RocketChat = {
let loginServices = [];
const loginServicesResult = await fetch(`${ server }/api/v1/settings.oauth`).then(response => response.json());
if (loginServicesResult.success && loginServicesResult.services.length > 0) {
if (loginServicesResult.success && loginServicesResult.services) {
const { services } = loginServicesResult;
loginServices = services;
@ -1066,42 +1066,9 @@ const RocketChat = {
this.activeUsers[ddpMessage.id] = ddpMessage.fields.status;
}
},
getUserPresence() {
return new Promise(async(resolve) => {
const serverVersion = reduxStore.getState().server.version;
// if server is lower than 1.1.0
if (serverVersion && semver.lt(semver.coerce(serverVersion), '1.1.0')) {
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
this.activeUsersSubTimeout = setTimeout(() => {
this.sdk.subscribe('activeUsers');
}, 5000);
return resolve();
} else {
const params = {};
// if (this.lastUserPresenceFetch) {
// params.from = this.lastUserPresenceFetch.toISOString();
// }
// RC 1.1.0
const result = await this.sdk.get('users.presence', params);
if (result.success) {
const activeUsers = result.users.reduce((ret, item) => {
ret[item._id] = item.status;
return ret;
}, {});
InteractionManager.runAfterInteractions(() => {
reduxStore.dispatch(setActiveUsers(activeUsers));
});
this.sdk.subscribe('stream-notify-logged', 'user-status');
return resolve();
}
}
});
},
getUsersPresence,
getUserPresence,
subscribeUsersPresence,
getDirectory({
query, count, offset, sort
}) {

View File

@ -6,10 +6,9 @@ import I18n from '../../i18n';
import styles from './styles';
import Markdown from '../../containers/markdown';
import { themes } from '../../constants/colors';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
const formatMsg = ({
lastMessage, type, showLastMessage, username
lastMessage, type, showLastMessage, username, useRealName
}) => {
if (!showLastMessage) {
return '';
@ -33,27 +32,25 @@ const formatMsg = ({
if (isLastMessageSentByMe) {
prefix = I18n.t('You_colon');
} else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `;
const { u: { name } } = lastMessage;
prefix = `${ useRealName ? name : lastMessage.u.username }: `;
}
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
if (msg) {
msg = shortnameToUnicode(msg);
}
return msg;
return `${ prefix }${ lastMessage.msg }`;
};
const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps);
const LastMessage = React.memo(({
lastMessage, type, showLastMessage, username, alert, theme
lastMessage, type, showLastMessage, username, alert, useRealName, theme
}) => (
<Markdown
msg={formatMsg({
lastMessage, type, showLastMessage, username
lastMessage, type, showLastMessage, username, useRealName
})}
style={[styles.markdownText, { color: alert ? themes[theme].bodyText : themes[theme].auxiliaryText }]}
customEmojis={false}
useRealName={useRealName}
numberOfLines={2}
preview
theme={theme}
@ -66,6 +63,7 @@ LastMessage.propTypes = {
type: PropTypes.string,
showLastMessage: PropTypes.bool,
username: PropTypes.string,
useRealName: PropTypes.bool,
alert: PropTypes.bool
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import { connect } from 'react-redux';
@ -20,6 +20,7 @@ const attrs = [
'unread',
'userMentions',
'showLastMessage',
'useRealName',
'alert',
'type',
'width',
@ -39,8 +40,15 @@ const arePropsEqual = (oldProps, newProps) => {
};
const RoomItem = React.memo(({
onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, theme
onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, useRealName, getUserPresence, theme
}) => {
useEffect(() => {
if (type === 'd' && rid) {
const uid = rid.replace(userId, '');
getUserPresence(uid);
}
}, []);
const date = formatDate(_updatedAt);
let accessibilityLabel = name;
@ -144,6 +152,7 @@ const RoomItem = React.memo(({
showLastMessage={showLastMessage}
username={username}
alert={alert && !hideUnreadStatus}
useRealName={useRealName}
theme={theme}
/>
<UnreadBadge
@ -187,12 +196,15 @@ RoomItem.propTypes = {
hideChannel: PropTypes.func,
avatar: PropTypes.bool,
hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool,
getUserPresence: PropTypes.func,
theme: PropTypes.string
};
RoomItem.defaultProps = {
avatarSize: 48,
status: 'offline'
status: 'offline',
getUserPresence: () => {}
};
const mapStateToProps = (state, ownProps) => ({

View File

@ -44,7 +44,9 @@ const UserItem = ({
}) => {
const longPress = ({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
onLongPress();
if (onLongPress) {
onLongPress();
}
}
};

View File

@ -2,6 +2,7 @@ import { combineReducers } from 'redux';
import settings from './reducers';
import login from './login';
import meteor from './connect';
import room from './room';
import rooms from './rooms';
import server from './server';
import selectedUsers from './selectedUsers';
@ -9,7 +10,6 @@ import createChannel from './createChannel';
import app from './app';
import sortPreferences from './sortPreferences';
import notification from './notification';
import markdown from './markdown';
import share from './share';
import crashReport from './crashReport';
import customEmojis from './customEmojis';
@ -25,10 +25,10 @@ export default combineReducers({
selectedUsers,
createChannel,
app,
room,
rooms,
sortPreferences,
notification,
markdown,
share,
crashReport,
customEmojis,

View File

@ -1,17 +0,0 @@
import { TOGGLE_MARKDOWN } from '../actions/actionsTypes';
const initialState = {
useMarkdown: true
};
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_MARKDOWN:
return {
useMarkdown: action.payload
};
default:
return state;
}
};

View File

@ -2,11 +2,6 @@ import * as types from '../constants/types';
import initialState from './initialState';
export default function settings(state = initialState.settings, action) {
if (action.type === types.SET_ALL_SETTINGS) {
return {
...action.payload
};
}
if (action.type === types.ADD_SETTINGS) {
return {
...state,

24
app/reducers/room.js Normal file
View File

@ -0,0 +1,24 @@
import { ROOM } from '../actions/actionsTypes';
const initialState = {
rid: null,
isDeleting: false
};
export default function(state = initialState, action) {
switch (action.type) {
case ROOM.DELETE_INIT:
return {
...state,
rid: action.rid,
isDeleting: true
};
case ROOM.DELETE_FINISH:
return {
...state,
isDeleting: false
};
default:
return state;
}
}

View File

@ -1,10 +1,12 @@
import {
select, put, call, take, takeLatest
} from 'redux-saga/effects';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';
import { createChannelSuccess, createChannelFailure } from '../actions/createChannel';
import RocketChat from '../lib/rocketchat';
import database from '../lib/database';
const create = function* create(data) {
return yield RocketChat.createChannel(data);
@ -16,8 +18,22 @@ const handleRequest = function* handleRequest({ data }) {
if (!auth) {
yield take(LOGIN.SUCCESS);
}
const result = yield call(create, data);
yield put(createChannelSuccess(result));
const sub = yield call(create, data);
try {
const db = database.active;
const subCollection = db.collections.get('subscriptions');
yield db.action(async() => {
await subCollection.create((s) => {
s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema);
Object.assign(s, sub);
});
});
} catch {
// do nothing
}
yield put(createChannelSuccess(sub));
} catch (err) {
yield put(createChannelFailure(err));
}

View File

@ -7,7 +7,6 @@ import RNBootSplash from 'react-native-bootsplash';
import * as actions from '../actions';
import { selectServerRequest } from '../actions/server';
import { setAllPreferences } from '../actions/sortPreferences';
import { toggleMarkdown } from '../actions/markdown';
import { toggleCrashReport } from '../actions/crashReport';
import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
@ -24,9 +23,6 @@ export const initLocalSettings = function* initLocalSettings() {
const sortPreferences = yield RocketChat.getSortPreferences();
yield put(setAllPreferences(sortPreferences));
const useMarkdown = yield RocketChat.getUseMarkdown();
yield put(toggleMarkdown(useMarkdown));
const allowCrashReport = yield RocketChat.getAllowCrashReport();
yield put(toggleCrashReport(allowCrashReport));
};
@ -114,15 +110,15 @@ const restore = function* restore() {
}
};
const start = function* start({ root }) {
const start = function* start({ root, text }) {
if (root === 'inside') {
yield Navigation.navigate('InsideStack');
} else if (root === 'setUsername') {
yield Navigation.navigate('SetUsernameView');
yield Navigation.navigate('SetUsernameStack');
} else if (root === 'outside') {
yield Navigation.navigate('OutsideStack');
} else if (root === 'loading') {
yield Navigation.navigate('AuthLoading');
yield Navigation.navigate('AuthLoading', { text });
}
RNBootSplash.hide();
};

View File

@ -35,7 +35,13 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE
} else {
result = yield call(loginWithPasswordCall, credentials);
}
return yield put(loginSuccess(result));
if (!result.username) {
yield put(serverFinishAdd());
yield put(setUser(result));
yield put(appStart('setUsername'));
} else {
yield put(loginSuccess(result));
}
} catch (e) {
if (logoutOnError && (e.data && e.data.message && /you've been logged out by the server/i.test(e.data.message))) {
yield put(logout(true));
@ -65,8 +71,9 @@ const registerPushToken = function* registerPushToken() {
yield RocketChat.registerPushToken();
};
const fetchUserPresence = function* fetchUserPresence() {
yield RocketChat.getUserPresence();
const fetchUsersPresence = function* fetchUserPresence() {
yield RocketChat.getUsersPresence();
yield RocketChat.subscribeUsersPresence();
};
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
@ -81,7 +88,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield fork(fetchRoles);
yield fork(fetchSlashCommands);
yield fork(registerPushToken);
yield fork(fetchUserPresence);
yield fork(fetchUsersPresence);
I18n.locale = user.language;
moment.locale(toMomentLocale(user.language));
@ -117,9 +124,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
EventEmitter.emit('connected');
let currentRoot;
if (!user.username) {
yield put(appStart('setUsername'));
} else if (adding) {
if (adding) {
yield put(serverFinishAdd());
yield put(appStart('inside'));
} else {
@ -143,7 +148,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
};
const handleLogout = function* handleLogout({ forcedByServer }) {
yield put(appStart('loading'));
yield put(appStart('loading', I18n.t('Logging_out')));
const server = yield select(getServer);
if (server) {
try {
@ -162,10 +167,12 @@ const handleLogout = function* handleLogout({ forcedByServer }) {
// see if there're other logged in servers and selects first one
if (servers.length > 0) {
const newServer = servers[0].id;
const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
for (let i = 0; i < servers.length; i += 1) {
const newServer = servers[i].id;
const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
}
}
}
// if there's no servers, go outside
@ -193,7 +200,6 @@ const root = function* root() {
while (true) {
const params = yield take(types.LOGIN.SUCCESS);
const loginSuccessTask = yield fork(handleLoginSuccess, params);
// yield take(types.SERVER.SELECT_REQUEST);
yield race({
selectRequest: take(types.SERVER.SELECT_REQUEST),
timeout: delay(2000)

View File

@ -1,10 +1,11 @@
import { Alert } from 'react-native';
import {
takeLatest, take, select, delay
takeLatest, take, select, delay, race, put
} from 'redux-saga/effects';
import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes';
import { deleteRoomFinish } from '../actions/room';
import RocketChat from '../lib/rocketchat';
import log from '../utils/log';
import I18n from '../i18n';
@ -42,20 +43,28 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
}
};
const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
const handleDeleteRoom = function* handleDeleteRoom({ rid, t }) {
try {
const result = yield RocketChat.eraseRoom(rid, t);
const result = yield RocketChat.deleteRoom(rid, t);
if (result.success) {
yield Navigation.navigate('RoomsListView');
}
// types.ROOM.DELETE_FINISH is triggered by `subscriptions-changed` with `removed` arg
const { timeout } = yield race({
deleteFinished: take(types.ROOM.DELETE_FINISH),
timeout: delay(3000)
});
if (timeout) {
put(deleteRoomFinish());
}
} catch (e) {
Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('erasing_room') }));
Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_room') }));
}
};
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
yield takeLatest(types.ROOM.DELETE_INIT, handleDeleteRoom);
};
export default root;

View File

@ -1,5 +1,5 @@
import {
put, take, takeLatest, fork, cancel, race
put, take, takeLatest, fork, cancel, race, select
} from 'redux-saga/effects';
import { Alert } from 'react-native';
import RNUserDefaults from 'rn-user-defaults';
@ -15,10 +15,11 @@ import {
import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat';
import database from '../lib/database';
import log from '../utils/log';
import log, { logServerVersion } from '../utils/log';
import { extractHostname } from '../utils/server';
import I18n from '../i18n';
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try {
@ -89,6 +90,9 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
}
}
const basicAuth = yield RNUserDefaults.get(`${ BASIC_AUTH_KEY }-${ server }`);
setBasicAuth(basicAuth);
if (user) {
yield RocketChat.connect({ server, user, logoutOnError: true });
yield put(setUser(user));
@ -109,7 +113,11 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
}
// Return server version even when offline
yield put(selectServerSuccess(server, (serverInfo && serverInfo.version) || version));
const serverVersion = (serverInfo && serverInfo.version) || version;
// we'll set serverVersion as metadata for bugsnag
logServerVersion(serverVersion);
yield put(selectServerSuccess(server, serverVersion));
} catch (e) {
yield put(selectServerFailure());
log(e);
@ -126,7 +134,11 @@ const handleServerRequest = function* handleServerRequest({ server, certificate
if (serverInfo) {
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
yield RocketChat.getLoginSettings({ server });
const showFormLogin = yield select(state => state.settings.Accounts_ShowFormLogin);
if (!loginServicesLength && showFormLogin) {
Navigation.navigate('LoginView');
} else {
Navigation.navigate('LoginSignupView');

View File

@ -15,7 +15,7 @@ import {
import Navigation from './lib/ShareNavigation';
import store from './lib/createStore';
import sharedStyles from './views/Styles';
import { isNotch, isIOS, supportSystemTheme } from './utils/deviceInfo';
import { isNotch, supportSystemTheme } from './utils/deviceInfo';
import { defaultHeader, onNavigationStateChange, cardStyle } from './utils/navigation';
import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat';
import { ThemeContext } from './theme';
@ -77,9 +77,6 @@ class Root extends React.Component {
}
init = async() => {
if (isIOS) {
await RNUserDefaults.setName('group.ios.chat.rocket');
}
RNUserDefaults.objectForKey(THEME_PREFERENCES_KEY).then(this.setTheme);
const currentServer = await RNUserDefaults.get('currentServer');
const token = await RNUserDefaults.get(RocketChat.TOKEN_KEY);

View File

@ -129,6 +129,17 @@ export const initTabletNav = (setState) => {
return null;
}
if (routeName === 'RoomsListView') {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'RoomView', params: {} })]
});
roomRef.dispatch(resetAction);
notificationRef.dispatch(resetAction);
setState({ showModal: false });
return null;
}
if (routeName === 'NewMessageView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });

View File

@ -1,13 +1,29 @@
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
// this form is required by Rocket.Chat's parser in "app/statistics/server/lib/UAParserCustom.js"
export const headers = { 'User-Agent': `RC Mobile; ${ Platform.OS } ${ DeviceInfo.getSystemVersion() }; v${ DeviceInfo.getVersion() } (${ DeviceInfo.getBuildNumber() })` };
export const headers = {
'User-Agent': `RC Mobile; ${ Platform.OS } ${ DeviceInfo.getSystemVersion() }; v${ DeviceInfo.getVersion() } (${ DeviceInfo.getBuildNumber() })`
};
let _basicAuth;
export const setBasicAuth = (basicAuth) => {
_basicAuth = basicAuth;
if (basicAuth) {
RocketChatSettings.customHeaders = { ...RocketChatSettings.customHeaders, Authorization: `Basic ${ _basicAuth }` };
} else {
RocketChatSettings.customHeaders = headers;
}
};
export const BASIC_AUTH_KEY = 'BASIC_AUTH_KEY';
RocketChatSettings.customHeaders = headers;
export default (url, options = {}) => {
let customOptions = { ...options, headers };
let customOptions = { ...options, headers: RocketChatSettings.customHeaders };
if (options && options.headers) {
customOptions = { ...customOptions, headers: { ...options.headers, ...headers } };
customOptions = { ...customOptions, headers: { ...options.headers, ...customOptions.headers } };
}
return fetch(url, customOptions);
};

View File

@ -5,6 +5,19 @@ import { isIOS } from './deviceInfo';
export const animateNextTransition = debounce(() => {
if (isIOS) {
LayoutAnimation.easeInEaseOut();
LayoutAnimation.configureNext({
duration: 200,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity
},
update: {
type: LayoutAnimation.Types.easeInEaseOut
},
delete: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity
}
});
}
}, 200, true);

View File

@ -8,9 +8,23 @@ export const { analytics } = firebase;
export const loggerConfig = bugsnag.config;
export const { leaveBreadcrumb } = bugsnag;
let metadata = {};
export const logServerVersion = (serverVersion) => {
metadata = {
serverVersion
};
};
export default (e) => {
if (e instanceof Error && !__DEV__) {
bugsnag.notify(e);
bugsnag.notify(e, (report) => {
report.metadata = {
details: {
...metadata
}
};
});
} else {
console.log(e);
}

View File

@ -7,7 +7,7 @@ export const canUploadFile = (file, serverInfo) => {
return { success: false, error: 'error-file-too-large' };
}
// if white list is empty, all media types are enabled
if (!FileUpload_MediaTypeWhiteList) {
if (!FileUpload_MediaTypeWhiteList || FileUpload_MediaTypeWhiteList === '*') {
return { success: true };
}
const allowedMime = FileUpload_MediaTypeWhiteList.split(',');

42
app/utils/messageTypes.js Normal file
View File

@ -0,0 +1,42 @@
export const MessageTypeValues = [
{
value: 'uj',
text: 'Message_HideType_uj'
}, {
value: 'ul',
text: 'Message_HideType_ul'
}, {
value: 'ru',
text: 'Message_HideType_ru'
}, {
value: 'au',
text: 'Message_HideType_au'
}, {
value: 'mute_unmute',
text: 'Message_HideType_mute_unmute'
}, {
value: 'r',
text: 'Message_HideType_r'
}, {
value: 'ut',
text: 'Message_HideType_ut'
}, {
value: 'wm',
text: 'Message_HideType_wm'
}, {
value: 'rm',
text: 'Message_HideType_rm'
}, {
value: 'subscription_role_added',
text: 'Message_HideType_subscription_role_added'
}, {
value: 'subscription_role_removed',
text: 'Message_HideType_subscription_role_removed'
}, {
value: 'room_archived',
text: 'Message_HideType_room_archived'
}, {
value: 'room_unarchived',
text: 'Message_HideType_room_unarchived'
}
];

View File

@ -1,12 +1,62 @@
import { Linking } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
import RNUserDefaults from 'rn-user-defaults';
import parse from 'url-parse';
import { themes } from '../constants/colors';
const openLink = (url, theme = 'light') => WebBrowser.openBrowserAsync(url, {
toolbarColor: themes[theme].headerBackground,
controlsColor: themes[theme].headerTintColor,
collapseToolbar: true,
showTitle: true
});
export const DEFAULT_BROWSER_KEY = 'DEFAULT_BROWSER_KEY';
const scheme = {
chrome: 'googlechrome:',
chromeSecure: 'googlechromes:',
firefox: 'firefox:',
brave: 'brave:'
};
const appSchemeURL = (url, browser) => {
let schemeUrl = url;
const parsedUrl = parse(url, true);
const { protocol } = parsedUrl;
const isSecure = ['https:'].includes(protocol);
if (browser === 'googlechrome') {
if (!isSecure) {
schemeUrl = url.replace(protocol, scheme.chrome);
} else {
schemeUrl = url.replace(protocol, scheme.chromeSecure);
}
} else if (browser === 'firefox') {
schemeUrl = `${ scheme.firefox }//open-url?url=${ url }`;
} else if (browser === 'brave') {
schemeUrl = `${ scheme.brave }//open-url?url=${ url }`;
}
return schemeUrl;
};
const openLink = async(url, theme = 'light') => {
try {
const browser = await RNUserDefaults.get(DEFAULT_BROWSER_KEY);
if (browser) {
const schemeUrl = appSchemeURL(url, browser.replace(':', ''));
await Linking.openURL(schemeUrl);
} else {
await WebBrowser.openBrowserAsync(url, {
toolbarColor: themes[theme].headerBackground,
controlsColor: themes[theme].headerTintColor,
collapseToolbar: true,
showTitle: true
});
}
} catch {
try {
await Linking.openURL(url);
} catch {
// do nothing
}
}
};
export default openLink;

View File

@ -65,8 +65,22 @@ class AttachmentView extends React.Component {
componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ handleSave: this.handleSave });
this.willBlurListener = navigation.addListener('willBlur', () => {
if (this.videoRef && this.videoRef.stopAsync) {
this.videoRef.stopAsync();
}
});
}
componentWillUnmount() {
if (this.willBlurListener && this.willBlurListener.remove) {
this.willBlurListener.remove();
}
}
getVideoRef = ref => this.videoRef = ref;
handleSave = async() => {
const { attachment } = this.state;
const { user, baseUrl } = this.props;
@ -117,6 +131,7 @@ class AttachmentView extends React.Component {
useNativeControls
onLoad={() => this.setState({ loading: false })}
onError={console.log}
ref={this.getVideoRef}
/>
);

View File

@ -1,10 +1,40 @@
import React from 'react';
import {
View, Text, StyleSheet, ActivityIndicator
} from 'react-native';
import I18n from '../i18n';
import StatusBar from '../containers/StatusBar';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
export default React.memo(withTheme(({ theme }) => (
<>
<StatusBar theme={theme} />
</>
)));
import sharedStyles from './Styles';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
text: {
fontSize: 16,
paddingTop: 10,
...sharedStyles.textRegular,
...sharedStyles.textAlignCenter
}
});
export default React.memo(withTheme(({ theme, navigation }) => {
const text = navigation.getParam('text');
return (
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
<StatusBar theme={theme} />
{text && (
<>
<ActivityIndicator color={themes[theme].auxiliaryText} size='large' />
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{`${ text }\n${ I18n.t('Please_wait') }`}</Text>
</>
)}
</View>
);
}));

View File

@ -208,10 +208,7 @@ class CreateChannelView extends React.Component {
}
removeUser = (user) => {
const { users, removeUser } = this.props;
if (users.length === 1) {
return;
}
const { removeUser } = this.props;
removeUser(user);
}
@ -285,6 +282,7 @@ class CreateChannelView extends React.Component {
username={item.name}
onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${ item.name }`}
icon='check'
baseUrl={baseUrl}
user={user}
theme={theme}

View File

@ -0,0 +1,191 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet, FlatList, View, Text, Linking
} from 'react-native';
import { SafeAreaView } from 'react-navigation';
import RNUserDefaults from 'rn-user-defaults';
import I18n from '../i18n';
import { themedHeader } from '../utils/navigation';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
import sharedStyles from './Styles';
import StatusBar from '../containers/StatusBar';
import Separator from '../containers/Separator';
import ListItem from '../containers/ListItem';
import { CustomIcon } from '../lib/Icons';
import { DEFAULT_BROWSER_KEY } from '../utils/openLink';
import { isIOS } from '../utils/deviceInfo';
const DEFAULT_BROWSERS = [
{
title: I18n.t('In_app'),
value: 'inApp'
},
{
title: isIOS ? 'Safari' : I18n.t('Browser'),
value: 'systemDefault:'
}
];
const BROWSERS = [
{
title: 'Chrome',
value: 'googlechrome:'
},
{
title: 'Firefox',
value: 'firefox:'
},
{
title: 'Brave',
value: 'brave:'
}
];
const styles = StyleSheet.create({
list: {
paddingBottom: 18
},
info: {
paddingTop: 25,
paddingBottom: 18,
paddingHorizontal: 16
},
infoText: {
fontSize: 16,
...sharedStyles.textRegular
}
});
class DefaultBrowserView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
title: I18n.t('Default_browser'),
...themedHeader(screenProps.theme)
})
static propTypes = {
theme: PropTypes.string
}
state = {
browser: null,
supported: []
}
constructor(props) {
super(props);
if (isIOS) {
this.init();
}
}
async componentDidMount() {
this.mounted = true;
try {
const browser = await RNUserDefaults.get(DEFAULT_BROWSER_KEY);
this.setState({ browser });
} catch {
// do nothing
}
}
init = () => {
BROWSERS.forEach((browser) => {
const { value } = browser;
Linking.canOpenURL(value).then((installed) => {
if (installed) {
if (this.mounted) {
this.setState(({ supported }) => ({ supported: [...supported, browser] }));
} else {
const { supported } = this.state;
this.state.supported = [...supported, browser];
}
}
});
});
}
isSelected = (value) => {
const { browser } = this.state;
if (!browser && value === 'inApp') {
return true;
}
return browser === value;
}
changeDefaultBrowser = async(newBrowser) => {
try {
const browser = newBrowser !== 'inApp' ? newBrowser : null;
await RNUserDefaults.set(DEFAULT_BROWSER_KEY, browser);
this.setState({ browser });
} catch {
// do nothing
}
}
renderSeparator = () => {
const { theme } = this.props;
return <Separator theme={theme} />;
}
renderIcon = () => {
const { theme } = this.props;
return <CustomIcon name='check' size={20} color={themes[theme].tintColor} />;
}
renderItem = ({ item }) => {
const { theme } = this.props;
const { title, value } = item;
return (
<ListItem
title={title}
onPress={() => this.changeDefaultBrowser(value)}
testID={`default-browser-view-${ title }`}
right={this.isSelected(value) ? this.renderIcon : null}
theme={theme}
/>
);
}
renderHeader = () => {
const { theme } = this.props;
return (
<>
<View style={styles.info}>
<Text style={[styles.infoText, { color: themes[theme].infoText }]}>{I18n.t('Choose_where_you_want_links_be_opened')}</Text>
</View>
{this.renderSeparator()}
</>
);
}
render() {
const { supported } = this.state;
const { theme } = this.props;
return (
<SafeAreaView
style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}
forceInset={{ vertical: 'never' }}
testID='default-browser-view'
>
<StatusBar theme={theme} />
<FlatList
data={DEFAULT_BROWSERS.concat(supported)}
keyExtractor={item => item.value}
contentContainerStyle={[
styles.list,
{ borderColor: themes[theme].separatorColor }
]}
renderItem={this.renderItem}
ListHeaderComponent={this.renderHeader}
ListFooterComponent={this.renderSeparator}
ItemSeparatorComponent={this.renderSeparator}
/>
</SafeAreaView>
);
}
}
export default withTheme(DefaultBrowserView);

View File

@ -77,7 +77,7 @@ class ForgotPasswordView extends React.Component {
showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert'));
}
} catch (e) {
const msg = (e.data && e.data.error) || I18n.t('There_was_an_error_while_action', I18n.t('resetting_password'));
const msg = (e.data && e.data.error) || I18n.t('There_was_an_error_while_action', { action: I18n.t('resetting_password') });
showErrorAlert(msg, I18n.t('Alert'));
}
this.setState({ isFetching: false });

View File

@ -6,7 +6,6 @@ import { SafeAreaView } from 'react-navigation';
import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n';
import Loading from '../../containers/Loading';
import { showErrorAlert } from '../../utils/info';
import log from '../../utils/log';
import { setUser as setUserAction } from '../../actions/login';
@ -53,6 +52,9 @@ const LANGUAGES = [
}, {
label: 'Italiano',
value: 'it'
}, {
label: '日本語',
value: 'ja'
}
];
@ -72,13 +74,12 @@ class LanguageView extends React.Component {
constructor(props) {
super(props);
this.state = {
language: props.user ? props.user.language : 'en',
saving: false
language: props.user ? props.user.language : 'en'
};
}
shouldComponentUpdate(nextProps, nextState) {
const { language, saving } = this.state;
const { language } = this.state;
const { user, theme } = this.props;
if (nextProps.theme !== theme) {
return true;
@ -86,9 +87,6 @@ class LanguageView extends React.Component {
if (nextState.language !== language) {
return true;
}
if (nextState.saving !== saving) {
return true;
}
if (nextProps.user.language !== user.language) {
return true;
}
@ -105,9 +103,18 @@ class LanguageView extends React.Component {
return;
}
this.setState({ saving: true });
const { appStart } = this.props;
const { user, setUser, appStart } = this.props;
await appStart('loading', I18n.t('Change_language_loading'));
// shows loading for at least 300ms
await Promise.all([this.changeLanguage(language), new Promise(resolve => setTimeout(resolve, 300))]);
await appStart('inside');
}
changeLanguage = async(language) => {
const { user, setUser } = this.props;
const params = {};
@ -132,15 +139,10 @@ class LanguageView extends React.Component {
// do nothing
}
});
await appStart('loading');
await appStart('inside');
} catch (e) {
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
log(e);
}
this.setState({ saving: false });
}
renderSeparator = () => {
@ -171,7 +173,6 @@ class LanguageView extends React.Component {
}
render() {
const { saving } = this.state;
const { theme } = this.props;
return (
<SafeAreaView
@ -193,7 +194,6 @@ class LanguageView extends React.Component {
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
/>
<Loading visible={saving} />
</SafeAreaView>
);
}
@ -205,7 +205,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
setUser: params => dispatch(setUserAction(params)),
appStart: params => dispatch(appStartAction(params))
appStart: (...params) => dispatch(appStartAction(...params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView));

View File

@ -58,6 +58,11 @@ const styles = StyleSheet.create({
serviceName: {
...sharedStyles.textBold
},
registerDisabled: {
...sharedStyles.textRegular,
...sharedStyles.textAlignCenter,
fontSize: 16
},
servicesTogglerContainer: {
flexDirection: 'row',
alignItems: 'center',
@ -108,6 +113,9 @@ class LoginSignupView extends React.Component {
Gitlab_URL: PropTypes.string,
CAS_enabled: PropTypes.bool,
CAS_login_url: PropTypes.string,
Accounts_ShowFormLogin: PropTypes.bool,
Accounts_RegistrationForm: PropTypes.string,
Accounts_RegistrationForm_LinkReplacementText: PropTypes.string,
theme: PropTypes.string
}
@ -124,7 +132,7 @@ class LoginSignupView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { collapsed, servicesHeight } = this.state;
const {
server, Site_Name, services, theme
server, Site_Name, services, Accounts_ShowFormLogin, Accounts_RegistrationForm, Accounts_RegistrationForm_LinkReplacementText, theme
} = this.props;
if (nextState.collapsed !== collapsed) {
return true;
@ -141,6 +149,15 @@ class LoginSignupView extends React.Component {
if (nextProps.theme !== theme) {
return true;
}
if (nextProps.Accounts_ShowFormLogin !== Accounts_ShowFormLogin) {
return true;
}
if (nextProps.Accounts_RegistrationForm !== Accounts_RegistrationForm) {
return true;
}
if (nextProps.Accounts_RegistrationForm_LinkReplacementText !== Accounts_RegistrationForm_LinkReplacementText) {
return true;
}
if (!equal(nextProps.services, services)) {
return true;
}
@ -333,10 +350,12 @@ class LoginSignupView extends React.Component {
renderServicesSeparator = () => {
const { collapsed } = this.state;
const { services, theme } = this.props;
const {
services, theme, Accounts_ShowFormLogin, Accounts_RegistrationForm
} = this.props;
const { length } = Object.values(services);
if (length > 3) {
if (length > 3 && Accounts_ShowFormLogin && Accounts_RegistrationForm) {
return (
<View style={styles.servicesTogglerContainer}>
<View style={[styles.separatorLine, styles.separatorLineLeft, { backgroundColor: themes[theme].auxiliaryText }]} />
@ -358,6 +377,7 @@ class LoginSignupView extends React.Component {
let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `icon_${ name }`;
const isSaml = service.service === 'saml';
let onPress = () => {};
switch (service.authType) {
@ -383,8 +403,8 @@ class LoginSignupView extends React.Component {
name = name.charAt(0).toUpperCase() + name.slice(1);
const { CAS_enabled, theme } = this.props;
let buttonText;
if (service.service === 'saml' || (service.service === 'cas' && CAS_enabled)) {
buttonText = <Text style={styles.serviceName}>{name}</Text>;
if (isSaml || (service.service === 'cas' && CAS_enabled)) {
buttonText = <Text style={[styles.serviceName, isSaml && { color: service.buttonLabelColor }]}>{name}</Text>;
} else {
buttonText = (
<>
@ -396,7 +416,7 @@ class LoginSignupView extends React.Component {
<Touch
key={service.name}
onPress={onPress}
style={styles.serviceButton}
style={[styles.serviceButton, isSaml && { backgroundColor: service.buttonColor }]}
theme={theme}
>
<View style={[styles.serviceButtonContainer, { borderColor: themes[theme].borderColor }]}>
@ -409,15 +429,14 @@ class LoginSignupView extends React.Component {
renderServices = () => {
const { servicesHeight } = this.state;
const { services } = this.props;
const { services, Accounts_ShowFormLogin, Accounts_RegistrationForm } = this.props;
const { length } = Object.values(services);
const style = {
overflow: 'hidden',
height: servicesHeight
};
if (length > 3) {
if (length > 3 && Accounts_ShowFormLogin && Accounts_RegistrationForm) {
return (
<Animated.View style={style}>
{Object.values(services).map(service => this.renderItem(service))}
@ -431,6 +450,38 @@ class LoginSignupView extends React.Component {
);
}
renderLogin = () => {
const { Accounts_ShowFormLogin, theme } = this.props;
if (!Accounts_ShowFormLogin) {
return null;
}
return (
<Button
title={<Text>{I18n.t('Login_with')} <Text style={{ ...sharedStyles.textBold }}>{I18n.t('email')}</Text></Text>}
type='primary'
onPress={() => this.login()}
theme={theme}
testID='welcome-view-login'
/>
);
}
renderRegister = () => {
const { Accounts_RegistrationForm, Accounts_RegistrationForm_LinkReplacementText, theme } = this.props;
if (Accounts_RegistrationForm !== 'Public') {
return <Text style={[styles.registerDisabled, { color: themes[theme].auxiliaryText }]}>{Accounts_RegistrationForm_LinkReplacementText}</Text>;
}
return (
<Button
title={I18n.t('Create_account')}
type='secondary'
onPress={() => this.register()}
theme={theme}
testID='welcome-view-register'
/>
);
}
render() {
const { theme } = this.props;
return (
@ -452,20 +503,8 @@ class LoginSignupView extends React.Component {
<StatusBar theme={theme} />
{this.renderServices()}
{this.renderServicesSeparator()}
<Button
title={<Text>{I18n.t('Login_with')} <Text style={{ ...sharedStyles.textBold }}>{I18n.t('email')}</Text></Text>}
type='primary'
onPress={() => this.login()}
theme={theme}
testID='welcome-view-login'
/>
<Button
title={I18n.t('Create_account')}
type='secondary'
onPress={() => this.register()}
theme={theme}
testID='welcome-view-register'
/>
{this.renderLogin()}
{this.renderRegister()}
</ScrollView>
</SafeAreaView>
);
@ -478,6 +517,9 @@ const mapStateToProps = state => ({
Gitlab_URL: state.settings.API_Gitlab_URL,
CAS_enabled: state.settings.CAS_enabled,
CAS_login_url: state.settings.CAS_login_url,
Accounts_ShowFormLogin: state.settings.Accounts_ShowFormLogin,
Accounts_RegistrationForm: state.settings.Accounts_RegistrationForm,
Accounts_RegistrationForm_LinkReplacementText: state.settings.Accounts_RegistrationForm_LinkReplacementText,
services: state.login.services
});

View File

@ -29,6 +29,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginTop: 10
},
registerDisabled: {
...sharedStyles.textRegular,
...sharedStyles.textAlignCenter,
fontSize: 13
},
dontHaveAccount: {
...sharedStyles.textRegular,
fontSize: 13
@ -61,6 +66,8 @@ class LoginView extends React.Component {
Accounts_EmailOrUsernamePlaceholder: PropTypes.string,
Accounts_PasswordPlaceholder: PropTypes.string,
Accounts_PasswordReset: PropTypes.bool,
Accounts_RegistrationForm: PropTypes.string,
Accounts_RegistrationForm_LinkReplacementText: PropTypes.string,
isFetching: PropTypes.bool,
failure: PropTypes.bool,
theme: PropTypes.string
@ -101,7 +108,7 @@ class LoginView extends React.Component {
user, password, code, showTOTP
} = this.state;
const {
isFetching, failure, error, Site_Name, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, theme
isFetching, failure, error, Site_Name, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_RegistrationForm, Accounts_RegistrationForm_LinkReplacementText, theme
} = this.props;
if (nextState.user !== user) {
return true;
@ -133,6 +140,12 @@ class LoginView extends React.Component {
if (nextProps.Accounts_PasswordPlaceholder !== Accounts_PasswordPlaceholder) {
return true;
}
if (nextProps.Accounts_RegistrationForm !== Accounts_RegistrationForm) {
return true;
}
if (nextProps.Accounts_RegistrationForm_LinkReplacementText !== Accounts_RegistrationForm_LinkReplacementText) {
return true;
}
if (!equal(nextProps.error, error)) {
return true;
}
@ -225,7 +238,7 @@ class LoginView extends React.Component {
renderUserForm = () => {
const {
Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_PasswordReset, isFetching, theme
Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_PasswordReset, Accounts_RegistrationForm, Accounts_RegistrationForm_LinkReplacementText, isFetching, theme
} = this.props;
return (
<SafeAreaView
@ -283,15 +296,17 @@ class LoginView extends React.Component {
theme={theme}
/>
)}
<View style={styles.bottomContainer}>
<Text style={[styles.dontHaveAccount, { color: themes[theme].auxiliaryText }]}>{I18n.t('Dont_Have_An_Account')}</Text>
<Text
style={[styles.createAccount, { color: themes[theme].actionTintColor }]}
onPress={this.register}
testID='login-view-register'
>{I18n.t('Create_account')}
</Text>
</View>
{Accounts_RegistrationForm === 'Public' ? (
<View style={styles.bottomContainer}>
<Text style={[styles.dontHaveAccount, { color: themes[theme].auxiliaryText }]}>{I18n.t('Dont_Have_An_Account')}</Text>
<Text
style={[styles.createAccount, { color: themes[theme].actionTintColor }]}
onPress={this.register}
testID='login-view-register'
>{I18n.t('Create_account')}
</Text>
</View>
) : (<Text style={[styles.registerDisabled, { color: themes[theme].auxiliaryText }]}>{Accounts_RegistrationForm_LinkReplacementText}</Text>)}
</SafeAreaView>
);
}
@ -323,6 +338,8 @@ const mapStateToProps = state => ({
Site_Name: state.settings.Site_Name,
Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder,
Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder,
Accounts_RegistrationForm: state.settings.Accounts_RegistrationForm,
Accounts_RegistrationForm_LinkReplacementText: state.settings.Accounts_RegistrationForm_LinkReplacementText,
Accounts_PasswordReset: state.settings.Accounts_PasswordReset
});

View File

@ -9,6 +9,9 @@ import * as FileSystem from 'expo-file-system';
import DocumentPicker from 'react-native-document-picker';
import ActionSheet from 'react-native-action-sheet';
import isEqual from 'deep-equal';
import RNUserDefaults from 'rn-user-defaults';
import { encode } from 'base-64';
import parse from 'url-parse';
import { serverRequest } from '../actions/server';
import sharedStyles from './Styles';
@ -25,6 +28,7 @@ import { themes } from '../constants/colors';
import log from '../utils/log';
import { animateNextTransition } from '../utils/layoutAnimation';
import { withTheme } from '../theme';
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
const styles = StyleSheet.create({
image: {
@ -148,7 +152,22 @@ class NewServerView extends React.Component {
if (text) {
Keyboard.dismiss();
connectServer(this.completeUrl(text), cert);
const server = this.completeUrl(text);
await this.basicAuth(server, text);
connectServer(server, cert);
}
}
basicAuth = async(server, text) => {
try {
const parsedUrl = parse(text, true);
if (parsedUrl.auth.length) {
const credentials = encode(parsedUrl.auth);
await RNUserDefaults.set(`${ BASIC_AUTH_KEY }-${ server }`, credentials);
setBasicAuth(credentials);
}
} catch {
// do nothing
}
}
@ -177,6 +196,11 @@ class NewServerView extends React.Component {
}
completeUrl = (url) => {
const parsedUrl = parse(url, true);
if (parsedUrl.auth.length) {
url = parsedUrl.host;
}
url = url && url.replace(/\s/g, '');
if (/^(\w|[0-9-_]){3,}$/.test(url)

View File

@ -242,7 +242,6 @@ class NotificationPreferencesView extends React.Component {
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
testID='notification-preference-view-list'
>
<Separator theme={theme} />

View File

@ -1,6 +1,6 @@
import React from 'react';
import {
View, Text, Image, TouchableOpacity, BackHandler
View, Text, Image, TouchableOpacity, BackHandler, Linking
} from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@ -10,7 +10,6 @@ import Orientation from 'react-native-orientation-locker';
import { selectServerRequest, serverInitAdd, serverFinishAdd } from '../../actions/server';
import { appStart as appStartAction } from '../../actions';
import I18n from '../../i18n';
import openLink from '../../utils/openLink';
import Button from './Button';
import styles from './styles';
import { isIOS, isNotch, isTablet } from '../../utils/deviceInfo';
@ -105,9 +104,12 @@ class OnboardingView extends React.Component {
this.newServer('https://open.rocket.chat');
}
createWorkspace = () => {
const { theme } = this.props;
openLink('https://cloud.rocket.chat/trial', theme);
createWorkspace = async() => {
try {
await Linking.openURL('https://cloud.rocket.chat/trial');
} catch {
// do nothing
}
}
renderClose = () => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, ScrollView, Keyboard } from 'react-native';
import { connect } from 'react-redux';
import Dialog from 'react-native-dialog';
import prompt from 'react-native-prompt-android';
import SHA256 from 'js-sha256';
import ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select';
@ -50,13 +50,17 @@ class ProfileView extends React.Component {
static propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
Accounts_AllowEmailChange: PropTypes.bool,
Accounts_AllowPasswordChange: PropTypes.bool,
Accounts_AllowRealNameChange: PropTypes.bool,
Accounts_AllowUserAvatarChange: PropTypes.bool,
Accounts_AllowUsernameChange: PropTypes.bool,
Accounts_CustomFields: PropTypes.string,
setUser: PropTypes.func,
theme: PropTypes.string
}
state = {
showPasswordAlert: false,
saving: false,
name: null,
username: null,
@ -98,6 +102,12 @@ class ProfileView extends React.Component {
}
setAvatar = (avatar) => {
const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) {
return;
}
this.setState({ avatar });
}
@ -144,19 +154,11 @@ class ProfileView extends React.Component {
);
}
closePasswordAlert = () => {
this.setState({ showPasswordAlert: false });
}
handleError = (e, func, action) => {
if (e.data && e.data.errorType === 'error-too-many-requests') {
return showErrorAlert(e.data.error);
}
showErrorAlert(
I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }),
'',
() => this.setState({ showPasswordAlert: false })
);
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
}
submit = async() => {
@ -201,7 +203,26 @@ class ProfileView extends React.Component {
const requirePassword = !!params.email || newPassword;
if (requirePassword && !params.currentPassword) {
return this.setState({ showPasswordAlert: true, saving: false });
this.setState({ saving: false });
prompt(
I18n.t('Please_enter_your_password'),
I18n.t('For_your_security_you_must_enter_your_current_password_to_continue'),
[
{ text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' },
{
text: I18n.t('Save'),
onPress: (p) => {
this.setState({ currentPassword: p });
this.submit();
}
}
],
{
type: 'secure-text',
cancelable: false
}
);
return;
}
try {
@ -222,7 +243,7 @@ class ProfileView extends React.Component {
} else {
setUser({ ...params });
}
this.setState({ saving: false, showPasswordAlert: false });
this.setState({ saving: false });
EventEmitter.emit(LISTENER, { message: I18n.t('Profile_saved_successfully') });
this.init();
}
@ -233,6 +254,12 @@ class ProfileView extends React.Component {
}
resetAvatar = async() => {
const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) {
return;
}
try {
const { user } = this.props;
await RocketChat.resetAvatar(user.id);
@ -244,6 +271,12 @@ class ProfileView extends React.Component {
}
pickImage = async() => {
const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) {
return;
}
const options = {
cropping: true,
compressImageQuality: 0.8,
@ -280,18 +313,25 @@ class ProfileView extends React.Component {
renderAvatarButtons = () => {
const { avatarUrl, avatarSuggestions } = this.state;
const { user, baseUrl, theme } = this.props;
const {
user,
baseUrl,
theme,
Accounts_AllowUserAvatarChange
} = this.props;
return (
<View style={styles.avatarButtons}>
{this.renderAvatarButton({
child: <Avatar text={`@${ user.username }`} size={50} baseUrl={baseUrl} userId={user.id} token={user.token} />,
onPress: () => this.resetAvatar(),
disabled: !Accounts_AllowUserAvatarChange,
key: 'profile-view-reset-avatar'
})}
{this.renderAvatarButton({
child: <CustomIcon name='upload' size={30} color={themes[theme].bodyText} />,
onPress: () => this.pickImage(),
disabled: !Accounts_AllowUserAvatarChange,
key: 'profile-view-upload-avatar'
})}
{this.renderAvatarButton({
@ -303,6 +343,7 @@ class ProfileView extends React.Component {
{Object.keys(avatarSuggestions).map((service) => {
const { url, blob, contentType } = avatarSuggestions[service];
return this.renderAvatarButton({
disabled: !Accounts_AllowUserAvatarChange,
key: `profile-view-avatar-${ service }`,
child: <Avatar avatar={url} size={50} baseUrl={baseUrl} userId={user.id} token={user.token} />,
onPress: () => this.setAvatar({
@ -378,10 +419,18 @@ class ProfileView extends React.Component {
render() {
const {
name, username, email, newPassword, avatarUrl, customFields, avatar, saving, showPasswordAlert
name, username, email, newPassword, avatarUrl, customFields, avatar, saving
} = this.state;
const {
baseUrl, user, theme, Accounts_CustomFields
baseUrl,
user,
theme,
Accounts_AllowEmailChange,
Accounts_AllowPasswordChange,
Accounts_AllowRealNameChange,
Accounts_AllowUserAvatarChange,
Accounts_AllowUsernameChange,
Accounts_CustomFields
} = this.props;
return (
@ -408,6 +457,10 @@ class ProfileView extends React.Component {
/>
</View>
<RCTextInput
editable={Accounts_AllowRealNameChange}
inputStyle={[
!Accounts_AllowRealNameChange && styles.disabled
]}
inputRef={(e) => { this.name = e; }}
label={I18n.t('Name')}
placeholder={I18n.t('Name')}
@ -418,6 +471,10 @@ class ProfileView extends React.Component {
theme={theme}
/>
<RCTextInput
editable={Accounts_AllowUsernameChange}
inputStyle={[
!Accounts_AllowUsernameChange && styles.disabled
]}
inputRef={(e) => { this.username = e; }}
label={I18n.t('Username')}
placeholder={I18n.t('Username')}
@ -428,6 +485,10 @@ class ProfileView extends React.Component {
theme={theme}
/>
<RCTextInput
editable={Accounts_AllowEmailChange}
inputStyle={[
!Accounts_AllowEmailChange && styles.disabled
]}
inputRef={(e) => { this.email = e; }}
label={I18n.t('Email')}
placeholder={I18n.t('Email')}
@ -438,6 +499,10 @@ class ProfileView extends React.Component {
theme={theme}
/>
<RCTextInput
editable={Accounts_AllowPasswordChange}
inputStyle={[
!Accounts_AllowPasswordChange && styles.disabled
]}
inputRef={(e) => { this.newPassword = e; }}
label={I18n.t('New_Password')}
placeholder={I18n.t('New_Password')}
@ -455,6 +520,10 @@ class ProfileView extends React.Component {
/>
{this.renderCustomFields()}
<RCTextInput
editable={Accounts_AllowUserAvatarChange}
inputStyle={[
!Accounts_AllowUserAvatarChange && styles.disabled
]}
inputRef={(e) => { this.avatarUrl = e; }}
label={I18n.t('Avatar_Url')}
placeholder={I18n.t('Avatar_Url')}
@ -474,22 +543,6 @@ class ProfileView extends React.Component {
loading={saving}
theme={theme}
/>
<Dialog.Container visible={showPasswordAlert}>
<Dialog.Title>
{I18n.t('Please_enter_your_password')}
</Dialog.Title>
<Dialog.Description>
{I18n.t('For_your_security_you_must_enter_your_current_password_to_continue')}
</Dialog.Description>
<Dialog.Input
onChangeText={value => this.setState({ currentPassword: value })}
secureTextEntry
testID='profile-view-typed-password'
style={styles.dialogInput}
/>
<Dialog.Button label={I18n.t('Cancel')} onPress={this.closePasswordAlert} />
<Dialog.Button label={I18n.t('Save')} onPress={this.submit} />
</Dialog.Container>
</ScrollView>
</SafeAreaView>
</KeyboardView>
@ -499,6 +552,11 @@ class ProfileView extends React.Component {
const mapStateToProps = state => ({
user: getUserSelector(state),
Accounts_AllowEmailChange: state.settings.Accounts_AllowEmailChange,
Accounts_AllowPasswordChange: state.settings.Accounts_AllowPasswordChange,
Accounts_AllowRealNameChange: state.settings.Accounts_AllowRealNameChange,
Accounts_AllowUserAvatarChange: state.settings.Accounts_AllowUserAvatarChange,
Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange,
Accounts_CustomFields: state.settings.Accounts_CustomFields,
baseUrl: state.server.server
});

View File

@ -1,6 +1,9 @@
import { StyleSheet, Platform } from 'react-native';
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
disabled: {
opacity: 0.3
},
avatarContainer: {
alignItems: 'center',
justifyContent: 'center',
@ -20,14 +23,5 @@ export default StyleSheet.create({
marginRight: 15,
marginBottom: 15,
borderRadius: 2
},
dialogInput: Platform.select({
ios: {},
android: {
borderRadius: 4,
borderColor: 'rgba(0,0,0,.15)',
borderWidth: 2,
paddingHorizontal: 10
}
})
}
});

View File

@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Keyboard, Text, ScrollView, Alert
} from 'react-native';
import { Keyboard, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import RNPickerSelect from 'react-native-picker-select';
@ -24,6 +22,7 @@ import { withTheme } from '../theme';
import { themes } from '../constants/colors';
import { themedHeader } from '../utils/navigation';
import { isTablet } from '../utils/deviceInfo';
import { showErrorAlert } from '../utils/info';
const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving'];
@ -129,12 +128,15 @@ class RegisterView extends React.Component {
if (Accounts_EmailVerification) {
await navigation.goBack();
Alert.alert(I18n.t('Verify_email_title'), I18n.t('Verify_email_desc'));
showErrorAlert(I18n.t('Verify_email_desc'), I18n.t('Verify_email_title'));
} else {
await loginRequest({ user: email, password });
}
} catch (e) {
Alert.alert(I18n.t('Oops'), e.data.error);
if (e.data && e.data.errorType === 'username-invalid') {
return loginRequest({ user: email, password });
}
showErrorAlert(e.data.error, I18n.t('Oops'));
}
this.setState({ saving: false });
}

View File

@ -5,45 +5,50 @@ import PropTypes from 'prop-types';
import styles from './styles';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
export default class SwitchContainer extends React.PureComponent {
static propTypes = {
value: PropTypes.bool,
disabled: PropTypes.bool,
leftLabelPrimary: PropTypes.string,
leftLabelSecondary: PropTypes.string,
rightLabelPrimary: PropTypes.string,
rightLabelSecondary: PropTypes.string,
onValueChange: PropTypes.func,
theme: PropTypes.string,
testID: PropTypes.string
}
const SwitchContainer = React.memo(({
children, value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary, theme, testID, labelContainerStyle, leftLabelStyle
}) => (
<>
<View key='switch-container' style={[styles.switchContainer, children && styles.switchMargin]}>
{leftLabelPrimary && (
<View style={[styles.switchLabelContainer, labelContainerStyle]}>
<Text style={[styles.switchLabelPrimary, { color: themes[theme].titleText }, leftLabelStyle]}>{leftLabelPrimary}</Text>
<Text style={[styles.switchLabelSecondary, { color: themes[theme].titleText }, leftLabelStyle]}>{leftLabelSecondary}</Text>
</View>
)}
<Switch
style={styles.switch}
onValueChange={onValueChange}
value={value}
disabled={disabled}
trackColor={SWITCH_TRACK_COLOR}
testID={testID}
/>
{rightLabelPrimary && (
<View style={[styles.switchLabelContainer, labelContainerStyle]}>
<Text style={[styles.switchLabelPrimary, { color: themes[theme].titleText }, leftLabelStyle]}>{rightLabelPrimary}</Text>
<Text style={[styles.switchLabelSecondary, { color: themes[theme].titleText }, leftLabelStyle]}>{rightLabelSecondary}</Text>
</View>
)}
</View>
{children}
<View key='switch-divider' style={[styles.divider, { borderColor: themes[theme].separatorColor }]} />
</>
));
render() {
const {
value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary, theme, testID
} = this.props;
return (
[
<View key='switch-container' style={styles.switchContainer}>
<View style={styles.switchLabelContainer}>
<Text style={[styles.switchLabelPrimary, { color: themes[theme].titleText }]}>{leftLabelPrimary}</Text>
<Text style={[styles.switchLabelSecondary, { color: themes[theme].titleText }]}>{leftLabelSecondary}</Text>
</View>
<Switch
style={styles.switch}
onValueChange={onValueChange}
value={value}
disabled={disabled}
trackColor={SWITCH_TRACK_COLOR}
testID={testID}
/>
<View style={styles.switchLabelContainer}>
<Text style={[styles.switchLabelPrimary, { color: themes[theme].titleText }]}>{rightLabelPrimary}</Text>
<Text style={[styles.switchLabelSecondary, { color: themes[theme].titleText }]}>{rightLabelSecondary}</Text>
</View>
</View>,
<View key='switch-divider' style={[styles.divider, { borderColor: themes[theme].separatorColor }]} />
]
);
}
}
SwitchContainer.propTypes = {
value: PropTypes.bool,
disabled: PropTypes.bool,
leftLabelPrimary: PropTypes.string,
leftLabelSecondary: PropTypes.string,
rightLabelPrimary: PropTypes.string,
rightLabelSecondary: PropTypes.string,
onValueChange: PropTypes.func,
theme: PropTypes.string,
testID: PropTypes.string,
labelContainerStyle: PropTypes.object,
leftLabelStyle: PropTypes.object,
children: PropTypes.any
};
export default SwitchContainer;

View File

@ -6,9 +6,12 @@ import {
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import isEqual from 'lodash/isEqual';
import semver from 'semver';
import database from '../../lib/database';
import { eraseRoom as eraseRoomAction } from '../../actions/room';
import { deleteRoomInit as deleteRoomInitAction } from '../../actions/room';
import KeyboardView from '../../presentation/KeyboardView';
import sharedStyles from '../Styles';
import styles from './styles';
@ -27,6 +30,8 @@ import StatusBar from '../../containers/StatusBar';
import { themedHeader } from '../../utils/navigation';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { MultiSelect } from '../../containers/UIKit/MultiSelect';
import { MessageTypeValues } from '../../utils/messageTypes';
const PERMISSION_SET_READONLY = 'set-readonly';
const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly';
@ -51,7 +56,8 @@ class RoomInfoEditView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
eraseRoom: PropTypes.func,
deleteRoomInit: PropTypes.func,
serverVersion: PropTypes.string,
theme: PropTypes.string
};
@ -70,7 +76,9 @@ class RoomInfoEditView extends React.Component {
t: false,
ro: false,
reactWhenReadOnly: false,
archived: false
archived: false,
systemMessages: [],
enableSysMes: false
};
this.loadRoom();
}
@ -117,7 +125,7 @@ class RoomInfoEditView extends React.Component {
init = (room) => {
const {
name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired
name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, sysMes
} = room;
// fake password just to user knows about it
this.randomValue = random(15);
@ -131,7 +139,9 @@ class RoomInfoEditView extends React.Component {
ro,
reactWhenReadOnly,
joinCode: joinCodeRequired ? this.randomValue : '',
archived: room.archived
archived: room.archived,
systemMessages: sysMes,
enableSysMes: sysMes && sysMes.length > 0
});
}
@ -148,7 +158,7 @@ class RoomInfoEditView extends React.Component {
formIsChanged = () => {
const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, enableSysMes
} = this.state;
const { joinCodeRequired } = room;
return !(room.name === name
@ -159,13 +169,15 @@ class RoomInfoEditView extends React.Component {
&& room.t === 'p' === t
&& room.ro === ro
&& room.reactWhenReadOnly === reactWhenReadOnly
&& isEqual(room.sysMes, systemMessages)
&& enableSysMes === (room.sysMes && room.sysMes.length > 0)
);
}
submit = async() => {
Keyboard.dismiss();
const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages
} = this.state;
this.setState({ saving: true });
@ -210,6 +222,10 @@ class RoomInfoEditView extends React.Component {
params.reactWhenReadOnly = reactWhenReadOnly;
}
if (!isEqual(room.sysMes, systemMessages)) {
params.systemMessages = systemMessages;
}
// Join Code
if (this.randomValue !== joinCode) {
params.joinCode = joinCode;
@ -237,7 +253,7 @@ class RoomInfoEditView extends React.Component {
delete = () => {
const { room } = this.state;
const { eraseRoom } = this.props;
const { deleteRoomInit } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
@ -250,7 +266,7 @@ class RoomInfoEditView extends React.Component {
{
text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
style: 'destructive',
onPress: () => eraseRoom(room.rid, room.t)
onPress: () => deleteRoomInit(room.rid, room.t)
}
],
{ cancelable: false }
@ -298,12 +314,34 @@ class RoomInfoEditView extends React.Component {
return (permissions[PERMISSION_ARCHIVE] || permissions[PERMISSION_UNARCHIVE]);
};
renderSystemMessages = () => {
const { systemMessages, enableSysMes } = this.state;
const { theme } = this.props;
if (!enableSysMes) {
return null;
}
return (
<MultiSelect
options={MessageTypeValues.map(m => ({ value: m.value, text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) } }))}
onChange={({ value }) => this.setState({ systemMessages: value })}
placeholder={{ text: I18n.t('Hide_System_Messages') }}
value={systemMessages}
context={BLOCK_CONTEXT.FORM}
multiselect
theme={theme}
/>
);
}
render() {
const {
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived, enableSysMes
} = this.state;
const { theme } = this.props;
const { serverVersion, theme } = this.props;
const { dangerColor } = themes[theme];
return (
<KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }}
@ -408,6 +446,20 @@ class RoomInfoEditView extends React.Component {
]
: null
}
{serverVersion && !semver.lt(serverVersion, '3.0.0') ? (
<SwitchContainer
value={enableSysMes}
leftLabelPrimary={I18n.t('Hide_System_Messages')}
leftLabelSecondary={enableSysMes ? I18n.t('Overwrites_the_server_configuration_and_use_room_config') : I18n.t('Uses_server_configuration')}
theme={theme}
testID='room-info-edit-switch-system-messages'
onValueChange={value => this.setState(({ systemMessages }) => ({ enableSysMes: value, systemMessages: value ? systemMessages : [] }))}
labelContainerStyle={styles.hideSystemMessages}
leftLabelStyle={styles.systemMessagesLabel}
>
{this.renderSystemMessages()}
</SwitchContainer>
) : null}
<TouchableOpacity
style={[
styles.buttonContainer,
@ -452,7 +504,7 @@ class RoomInfoEditView extends React.Component {
]}
onPress={this.toggleArchive}
disabled={!this.hasArchivePermission()}
testID='room-info-edit-view-archive'
testID={archived ? 'room-info-edit-view-unarchive' : 'room-info-edit-view-archive'}
>
<Text
style={[
@ -460,7 +512,6 @@ class RoomInfoEditView extends React.Component {
styles.button_inverted,
{ color: dangerColor }
]}
accessibilityTraits='button'
>
{ archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
</Text>
@ -498,8 +549,12 @@ class RoomInfoEditView extends React.Component {
}
}
const mapDispatchToProps = dispatch => ({
eraseRoom: (rid, t) => dispatch(eraseRoomAction(rid, t))
const mapStateToProps = state => ({
serverVersion: state.server.version
});
export default connect(null, mapDispatchToProps)(withTheme(RoomInfoEditView));
const mapDispatchToProps = dispatch => ({
deleteRoomInit: (rid, t) => dispatch(deleteRoomInitAction(rid, t))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomInfoEditView));

View File

@ -63,5 +63,14 @@ export default StyleSheet.create({
broadcast: {
...sharedStyles.textAlignCenter,
...sharedStyles.textSemibold
},
hideSystemMessages: {
alignItems: 'flex-start'
},
systemMessagesLabel: {
textAlign: 'left'
},
switchMargin: {
marginBottom: 16
}
});

Some files were not shown because too many files have changed in this diff Show More