Merge branch 'develop' into chore.try-flashlist

# Conflicts:
#	app/containers/message/index.tsx
#	app/views/RoomView/List/List.tsx
This commit is contained in:
Diego Mello 2022-11-21 18:25:08 -03:00
commit 898a4a398b
227 changed files with 5205 additions and 3083 deletions

View File

@ -3,5 +3,5 @@ updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
open-pull-requests-limit: 25 open-pull-requests-limit: 25

View File

@ -6,11 +6,13 @@ import RNBootSplash from 'react-native-bootsplash';
import { selectServerRequest } from '../app/actions/server'; import { selectServerRequest } from '../app/actions/server';
import { mockedStore as store } from '../app/reducers/mockedStore'; import { mockedStore as store } from '../app/reducers/mockedStore';
import database from '../app/lib/database'; import database from '../app/lib/database';
import { setUser } from '../app/actions/login';
RNBootSplash.hide(); RNBootSplash.hide();
const baseUrl = 'https://open.rocket.chat'; const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl)); store.dispatch(selectServerRequest(baseUrl));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
database.setActiveDB(baseUrl); database.setActiveDB(baseUrl);
const StorybookUIRoot = getStorybookUI({}); const StorybookUIRoot = getStorybookUI({});

View File

@ -3,7 +3,13 @@ import { Provider } from 'react-redux';
import { themes } from '../app/lib/constants'; import { themes } from '../app/lib/constants';
import MessageContext from '../app/containers/message/Context'; import MessageContext from '../app/containers/message/Context';
import { selectServerRequest } from '../app/actions/server';
import { mockedStore as store } from '../app/reducers/mockedStore'; import { mockedStore as store } from '../app/reducers/mockedStore';
import { setUser } from '../app/actions/login';
const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
export const decorators = [ export const decorators = [
Story => ( Story => (
@ -15,7 +21,7 @@ export const decorators = [
username: 'diego.mello', username: 'diego.mello',
token: 'abc' token: 'abc'
}, },
baseUrl: 'https://open.rocket.chat', baseUrl,
onPress: () => {}, onPress: () => {},
onLongPress: () => {}, onLongPress: () => {},
reactionInit: () => {}, reactionInit: () => {},

View File

@ -30,6 +30,7 @@ const getStories = () => {
require("../app/containers/markdown/new/NewMarkdown.stories.tsx"), require("../app/containers/markdown/new/NewMarkdown.stories.tsx"),
require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories.tsx"), require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories.tsx"),
require("../app/containers/message/Message.stories.tsx"), require("../app/containers/message/Message.stories.tsx"),
require("../app/containers/ReactionsList/ReactionsList.stories.tsx"),
require("../app/containers/RoomHeader/RoomHeader.stories.tsx"), require("../app/containers/RoomHeader/RoomHeader.stories.tsx"),
require("../app/containers/RoomItem/RoomItem.stories.tsx"), require("../app/containers/RoomItem/RoomItem.stories.tsx"),
require("../app/containers/SearchBox/SearchBox.stories.tsx"), require("../app/containers/SearchBox/SearchBox.stories.tsx"),

View File

@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Button Custom Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"purple\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"yellow\\",\\"fontSize\\":18},[{\\"textAlign\\":\\"left\\"}]],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Custom Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"purple\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"yellow\\",\\"fontSize\\":18},[{\\"textAlign\\":\\"left\\"}]],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button Disabled Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Disabled Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button Disabled Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`; exports[`Storyshots Button Disabled Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`;
exports[`Storyshots Button Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`; exports[`Storyshots Button Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`;
exports[`Storyshots Button Primary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Primary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button Secondary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Secondary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;

View File

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`; exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`; exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`;

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Login Services Separators 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"More options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"More options\\"},\\"children\\":[\\"More options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]},{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Less options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Less options\\"},\\"children\\":[\\"Less options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]}]"`; exports[`Storyshots Login Services Separators 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"More options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"More options\\"},\\"children\\":[\\"More options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]},{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Less options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Less options\\"},\\"children\\":[\\"Less options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]}]"`;
exports[`Storyshots Login Services Service List 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"github\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"gitlab\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"google\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"apple\\"]}]}]}]"`; exports[`Storyshots Login Services Service List 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"github\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"gitlab\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"google\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"apple\\"]}]}]}]"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots SearchBox Basic 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"testID\\":\\"searchbox\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},{\\"margin\\":16,\\"marginBottom\\":16}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"padding\\":14,\\"borderWidth\\":0.5,\\"borderRadius\\":2},null,{\\"paddingRight\\":45},{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"underlineColorAndroid\\":\\"transparent\\",\\"accessibilityLabel\\":\\"Search\\",\\"placeholder\\":\\"Search\\",\\"value\\":\\"\\",\\"blurOnSubmit\\":true,\\"returnKeyType\\":\\"search\\"},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":20,\\"color\\":\\"#2f343d\\"},[{\\"position\\":\\"absolute\\",\\"top\\":14},{\\"right\\":15}],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots SearchBox Basic 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"testID\\":\\"searchbox\\",\\"style\\":{\\"backgroundColor\\":\\"#ffffff\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},{\\"margin\\":16,\\"marginBottom\\":16}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\",\\"justifyContent\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"paddingHorizontal\\":16,\\"paddingVertical\\":10,\\"borderWidth\\":1,\\"borderRadius\\":4},null,{\\"paddingRight\\":45},{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"underlineColorAndroid\\":\\"transparent\\",\\"accessibilityLabel\\":\\"Search\\",\\"placeholder\\":\\"Search\\",\\"value\\":\\"\\",\\"blurOnSubmit\\":true,\\"returnKeyType\\":\\"search\\"},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":20,\\"color\\":\\"#2f343d\\"},[{\\"position\\":\\"absolute\\"},{\\"right\\":12}],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.31.0" versionName "4.33.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -351,6 +351,7 @@ android {
dependencies { dependencies {
implementation project(':@react-native-community_viewpager') implementation project(':@react-native-community_viewpager')
implementation "androidx.core:core-splashscreen:1.0.0"
playImplementation project(':react-native-notifications') playImplementation project(':react-native-notifications')
playImplementation 'com.google.firebase:firebase-core:16.0.0' playImplementation 'com.google.firebase:firebase-core:16.0.0'
playImplementation project(':@react-native-firebase_app') playImplementation project(':@react-native-firebase_app')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -13,17 +13,8 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/BootTheme"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity
android:name="com.zoontek.rnbootsplash.RNBootSplashActivity"
android:theme="@style/BootTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity <activity
android:name="chat.rocket.reactnative.MainActivity" android:name="chat.rocket.reactnative.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
@ -56,6 +47,10 @@
android:host="jitsi.rocket.chat" android:host="jitsi.rocket.chat"
android:scheme="rocketchat" /> android:scheme="rocketchat" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name="chat.rocket.reactnative.share.ShareActivity" android:name="chat.rocket.reactnative.share.ShareActivity"
@ -63,7 +58,7 @@
android:label="@string/share_extension_name" android:label="@string/share_extension_name"
android:noHistory="true" android:noHistory="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AppTheme" android:theme="@style/ShareTheme"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />

View File

@ -1,24 +1,23 @@
package chat.rocket.reactnative; package chat.rocket.reactnative;
import android.os.Bundle;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle;
import com.facebook.react.ReactRootView;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.zoontek.rnbootsplash.RNBootSplash;
import expo.modules.ReactActivityDelegateWrapper; import expo.modules.ReactActivityDelegateWrapper;
import com.zoontek.rnbootsplash.RNBootSplash;
public class MainActivity extends ReactActivity { public class MainActivity extends ReactActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
RNBootSplash.init(this);
// https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067 // https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
super.onCreate(null); super.onCreate(null);
RNBootSplash.init(R.drawable.launch_screen, MainActivity.this);
} }
@Override @Override

View File

@ -1,8 +1,6 @@
package chat.rocket.reactnative.share; package chat.rocket.reactnative.share;
import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
public class ShareActivity extends ReactActivity { public class ShareActivity extends ReactActivity {
@Override @Override

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
<item android:drawable="@color/splashBackground" />
<item>
<!-- the app logo, centered horizontally and vertically -->
<bitmap
android:src="@drawable/splash"
android:gravity="center" />
</item>
</layer-list>

View File

@ -8,25 +8,15 @@
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style> </style>
<style name="Share.Window" parent="android:Theme"> <style name="ShareTheme" parent="AppTheme">
<item name="android:windowEnterAnimation">@null</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowExitAnimation">@null</item> <item name="android:statusBarColor">@android:color/transparent</item>
</style> </style>
<style name="Theme.Share.Transparent" parent="android:Theme"> <style name="BootTheme" parent="Theme.SplashScreen">
<item name="android:windowIsTranslucent">true</item> <item name="windowSplashScreenBackground">@color/splashBackground</item>
<item name="android:windowBackground">@color/primary_dark</item> <item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="android:windowContentOverlay">@null</item> <item name="postSplashScreenTheme">@style/AppTheme</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/Share.Window</item>
</style>
<style name="BootTheme" parent="AppTheme">
<item name="android:background">@drawable/launch_screen</item>
<item name="colorPrimaryDark">@color/splashBackground</item>
<item name="android:navigationBarColor">@color/splashBackground</item>
</style> </style>
<!-- https://github.com/facebook/react-native/blob/d1ab03235cb4b93304150878d2b9057ab45bba77/ReactAndroid/src/main/res/views/modal/values/themes.xml#L5 --> <!-- https://github.com/facebook/react-native/blob/d1ab03235cb4b93304150878d2b9057ab45bba77/ReactAndroid/src/main/res/views/modal/values/themes.xml#L5 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
<item android:drawable="@color/splashBackground" />
<item>
<!-- the app logo, centered horizontally and vertically -->
<bitmap
android:src="@drawable/splash"
android:gravity="center" />
</item>
</layer-list>

View File

@ -6,7 +6,6 @@
android:name="chat.rocket.reactnative.MainPlayApplication" android:name="chat.rocket.reactnative.MainPlayApplication"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:name" tools:replace="android:name"
> >

View File

@ -1,5 +1,7 @@
package chat.rocket.reactnative; package chat.rocket.reactnative;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -35,8 +37,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification { public class CustomPushNotification extends PushNotification {
public static ReactApplicationContext reactApplicationContext; public static ReactApplicationContext reactApplicationContext;
final NotificationManager notificationManager; final NotificationManager notificationManager;
@ -322,7 +322,12 @@ public class CustomPushNotification extends PushNotification {
replyIntent.setAction(KEY_REPLY); replyIntent.setAction(KEY_REPLY);
replyIntent.putExtra("pushNotification", bundle); replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent replyPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
} else {
replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY)
.setLabel(label) .setLabel(label)
@ -343,12 +348,13 @@ public class CustomPushNotification extends PushNotification {
Intent intent = new Intent(mContext, DismissNotification.class); Intent intent = new Intent(mContext, DismissNotification.class);
intent.putExtra(NOTIFICATION_ID, notificationId); intent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, 0); PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
notification.setDeleteIntent(dismissPendingIntent); notification.setDeleteIntent(dismissPendingIntent);
} }
private void notificationLoad(Ejson ejson, Callback callback) { private void notificationLoad(Ejson ejson, Callback callback) {
LoadNotification.load(reactApplicationContext, ejson, callback); LoadNotification loadNotification = new LoadNotification();
loadNotification.load(reactApplicationContext, ejson, callback);
} }
} }

View File

@ -1,20 +1,14 @@
package chat.rocket.reactnative; package chat.rocket.reactnative;
import android.os.Bundle; import android.os.Bundle;
import android.content.Context;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Interceptor;
import com.google.gson.Gson;
import java.io.IOException;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.google.gson.Gson;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
class JsonResponse { class JsonResponse {
Data data; Data data;
@ -49,11 +43,11 @@ class JsonResponse {
} }
public class LoadNotification { public class LoadNotification {
private static int RETRY_COUNT = 0; private int RETRY_COUNT = 0;
private static int[] TIMEOUT = new int[]{0, 1, 3, 5, 10}; private int[] TIMEOUT = new int[]{0, 1, 3, 5, 10};
private static String TOKEN_KEY = "reactnativemeteor_usertoken-"; private String TOKEN_KEY = "reactnativemeteor_usertoken-";
public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) { public void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
final OkHttpClient client = new OkHttpClient(); final OkHttpClient client = new OkHttpClient();
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder(); HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
@ -73,7 +67,7 @@ public class LoadNotification {
runRequest(client, request, callback); runRequest(client, request, callback);
} }
private static void runRequest(OkHttpClient client, Request request, Callback callback) { private void runRequest(OkHttpClient client, Request request, Callback callback) {
try { try {
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000); Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);

View File

@ -18,10 +18,10 @@ const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MIN_SNAP_HEIGHT = 16; const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64; const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250; export const ACTION_SHEET_ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = { const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION, duration: ACTION_SHEET_ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic // https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0) easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
}; };
@ -140,7 +140,7 @@ const ActionSheet = React.memo(
style={{ ...styles.container, ...bottomSheet }} style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }} backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && onClose()} onChange={index => index === -1 && onClose()}
// We need this to allow horizontal swipe gestures inside bottom sheet like in reaction picker // We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
enableContentPanningGesture={data?.enableContentPanningGesture ?? true} enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet} {...androidTablet}
> >

View File

@ -122,7 +122,6 @@ const ActionSheetContentWithInputAndSubmit = ({
}} }}
testID={testID} testID={testID}
secureTextEntry={secureTextEntry} secureTextEntry={secureTextEntry}
inputStyle={{ borderWidth: 2 }}
bottomSheet={isIOS} bottomSheet={isIOS}
/> />
) : null} ) : null}

View File

@ -52,7 +52,11 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
/> />
); );
} }
return <BottomSheetView style={styles.contentContainer}>{children}</BottomSheetView>; return (
<BottomSheetView testID='action-sheet' style={styles.contentContainer}>
{children}
</BottomSheetView>
);
}); });
export default BottomSheetContent; export default BottomSheetContent;

View File

@ -1 +1,2 @@
export * from './Provider'; export * from './Provider';
export * from './ActionSheet';

View File

@ -52,7 +52,7 @@ export default StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
justifyContent: 'center', justifyContent: 'center',
height: ITEM_HEIGHT, height: ITEM_HEIGHT,
borderRadius: 2, borderRadius: 4,
marginBottom: 12 marginBottom: 12
}, },
text: { text: {

View File

@ -43,9 +43,7 @@ const Avatar = React.memo(
let image; let image;
if (emoji) { if (emoji) {
image = ( image = <Emoji getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />;
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
);
} else { } else {
let uri = avatar; let uri = avatar;
if (!isStatic) { if (!isStatic) {

View File

@ -22,7 +22,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
justifyContent: 'center', justifyContent: 'center',
height: 48, height: 48,
borderRadius: 2, borderRadius: 4,
marginBottom: 12 marginBottom: 12
}, },
text: { text: {

View File

@ -10,7 +10,7 @@ const styles = StyleSheet.create({
pressable: { pressable: {
paddingHorizontal: 8, paddingHorizontal: 8,
marginRight: 8, marginRight: 8,
borderRadius: 2, borderRadius: 4,
justifyContent: 'center', justifyContent: 'center',
maxWidth: 192 maxWidth: 192
}, },

View File

@ -1,24 +1,30 @@
import React from 'react'; import React from 'react';
import FastImage from 'react-native-fast-image'; import { StyleProp } from 'react-native';
import FastImage, { ImageStyle } from 'react-native-fast-image';
import { ICustomEmoji } from '../../definitions/IEmoji'; import { useAppSelector } from '../../lib/hooks';
import { ICustomEmoji } from '../../definitions';
interface ICustomEmojiProps {
emoji: ICustomEmoji;
style: StyleProp<ImageStyle>;
}
const CustomEmoji = React.memo( const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => ( ({ emoji, style }: ICustomEmojiProps) => {
<FastImage const baseUrl = useAppSelector(state => state.share.server.server || state.server.server);
style={style} return (
source={{ <FastImage
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`, style={style}
priority: FastImage.priority.high source={{
}} uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.name)}.${emoji.extension}`,
resizeMode={FastImage.resizeMode.contain} priority: FastImage.priority.high
/> }}
), resizeMode={FastImage.resizeMode.contain}
(prevProps, nextProps) => { />
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name; );
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name; },
return prevEmoji === nextEmoji; () => true
}
); );
export default CustomEmoji; export default CustomEmoji;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Text } from 'react-native';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { IEmoji } from '../../definitions/IEmoji';
interface IEmojiProps {
emoji: IEmoji;
}
export const Emoji = ({ emoji }: IEmojiProps): React.ReactElement => {
if (typeof emoji === 'string') {
return <Text style={styles.categoryEmoji}>{shortnameToUnicode(`:${emoji}:`)}</Text>;
}
return <CustomEmoji style={styles.customCategoryEmoji} emoji={emoji} />;
};

View File

@ -1,75 +1,45 @@
import React from 'react'; import React from 'react';
import { FlatList, Text, TouchableOpacity } from 'react-native'; import { useWindowDimensions } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import { EMOJI_BUTTON_SIZE } from './styles';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji'; import { IEmoji } from '../../definitions/IEmoji';
import { PressableEmoji } from './PressableEmoji';
const EMOJI_SIZE = 50; interface IEmojiCategoryProps {
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
tabLabel?: string; // needed for react-native-scrollable-tab-view only
}
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => { const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
if (emoji && emoji.isCustom) { const { width } = useWindowDimensions();
return (
<CustomEmoji const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
emoji={emoji}
baseUrl={baseUrl} const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
/>
); if (!width) {
return null;
} }
return ( return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}> <FlatList
{shortnameToUnicode(`:${emoji}:`)} // needed to update the numColumns when the width changes
</Text> key={`emoji-category-${width}`}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
data={emojis}
renderItem={renderItem}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
contentContainerStyle={{ marginHorizontal }}
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
); );
}; };
class EmojiCategory extends React.Component<IEmojiCategory> {
renderItem(emoji: IEmoji) {
const { baseUrl, onEmojiSelected } = this.props;
return (
<TouchableOpacity
activeOpacity={0.7}
// @ts-ignore
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}
>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity>
);
}
render() {
const { emojis, width } = this.props;
if (!width) {
return null;
}
const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return (
<FlatList
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
key={`emoji-category-${width}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis}
extraData={this.props}
renderItem={({ item }) => this.renderItem(item)}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
);
}
}
export default EmojiCategory; export default EmojiCategory;

View File

@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { StyleSheet, TextInputProps } from 'react-native';
import { FormTextInput } from '../TextInput/FormTextInput';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { isIOS } from '../../lib/methods/helpers';
const styles = StyleSheet.create({
input: {
height: 32,
borderWidth: 0,
paddingVertical: 0,
borderRadius: 4
},
textInputContainer: {
marginBottom: 0
}
});
interface IEmojiSearchBarProps {
onBlur?: TextInputProps['onBlur'];
onChangeText: TextInputProps['onChangeText'];
bottomSheet?: boolean;
}
export const EmojiSearch = ({ onBlur, onChangeText, bottomSheet }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const handleTextChange = (text: string) => {
setSearchText(text);
if (onChangeText) {
onChangeText(text);
}
};
return (
<FormTextInput
autoCapitalize='none'
autoCorrect={false}
autoComplete='off'
returnKeyType='search'
textContentType='none'
blurOnSubmit
placeholder={I18n.t('Search_emoji')}
placeholderTextColor={colors.auxiliaryText}
underlineColorAndroid='transparent'
onChangeText={handleTextChange}
inputStyle={[styles.input, { backgroundColor: colors.textInputSecondaryBackground }]}
containerStyle={styles.textInputContainer}
value={searchText}
onClearInput={() => handleTextChange('')}
onBlur={onBlur}
iconRight={'search'}
testID='emoji-searchbar-input'
bottomSheet={bottomSheet && isIOS}
autoFocus={!bottomSheet} // focus on input when not in reaction picker
/>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { View, Pressable } from 'react-native';
import { useTheme } from '../../theme';
import { CustomIcon } from '../CustomIcon';
import styles from './styles';
import { IFooterProps } from './interfaces';
const BUTTON_HIT_SLOP = { top: 15, right: 15, bottom: 15, left: 15 };
const Footer = ({ onSearchPressed, onBackspacePressed }: IFooterProps): React.ReactElement => {
const { colors } = useTheme();
return (
<View style={[styles.footerContainer, { borderTopColor: colors.borderColor }]}>
<Pressable
onPress={onSearchPressed}
hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-search'
>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' />
</Pressable>
<Pressable
onPress={onBackspacePressed}
hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-backspace'
>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='backspace' />
</Pressable>
</View>
);
};
export default Footer;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Pressable } from 'react-native';
import styles, { EMOJI_BUTTON_SIZE } from './styles';
import { IEmoji } from '../../definitions/IEmoji';
import { useTheme } from '../../theme';
import { isIOS } from '../../lib/methods/helpers';
import { Emoji } from './Emoji';
export const PressableEmoji = ({ emoji, onPress }: { emoji: IEmoji; onPress: (emoji: IEmoji) => void }): React.ReactElement => {
const { colors } = useTheme();
return (
<Pressable
key={typeof emoji === 'string' ? emoji : emoji.name}
onPress={() => onPress(emoji)}
testID={`emoji-${typeof emoji === 'string' ? emoji : emoji.name}`}
android_ripple={{ color: colors.bannerBackground, borderless: true, radius: EMOJI_BUTTON_SIZE / 2 }}
style={({ pressed }: { pressed: boolean }) => [
styles.emojiButton,
{
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<Emoji emoji={emoji} />
</Pressable>
);
};

View File

@ -1,56 +1,42 @@
import React from 'react'; import React from 'react';
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native'; import { Pressable, View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../lib/constants'; import { useTheme } from '../../theme';
import { TSupportedThemes } from '../../theme'; import { ITabBarProps } from './interfaces';
import { isIOS } from '../../lib/methods/helpers';
import { CustomIcon } from '../CustomIcon';
interface ITabBarProps { const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement => {
goToPage?: (page: number) => void; const { colors } = useTheme();
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
theme: TSupportedThemes;
}
export default class TabBar extends React.Component<ITabBarProps> { return (
shouldComponentUpdate(nextProps: ITabBarProps) { <View style={styles.tabsContainer}>
const { activeTab, theme } = this.props; {tabs?.map((tab, i) => (
if (nextProps.activeTab !== activeTab) { <Pressable
return true; key={tab}
} onPress={() => goToPage?.(i)}
if (nextProps.theme !== theme) { testID={`emoji-picker-tab-${tab}`}
return true; android_ripple={{ color: colors.bannerBackground }}
} style={({ pressed }: { pressed: boolean }) => [
return false; styles.tab,
} {
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<CustomIcon name={tab} size={24} color={activeTab === i ? colors.tintColor : colors.auxiliaryTintColor} />
<View
style={
activeTab === i
? [styles.activeTabLine, { backgroundColor: colors.tintColor }]
: [styles.tabLine, { backgroundColor: colors.borderColor }]
}
/>
</Pressable>
))}
</View>
);
};
render() { export default TabBar;
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
return (
<View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => {
if (goToPage) {
goToPage(i);
}
}}
style={styles.tab}
testID={`reaction-picker-${tab}`}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -1,155 +1,44 @@
import React, { Component } from 'react'; import React from 'react';
import { StyleProp, TextStyle, View } from 'react-native'; import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { ImageStyle } from 'react-native-fast-image';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import Footer from './Footer';
import styles from './styles'; import styles from './styles';
import categories from './categories'; import { categories, emojisByCategory } from '../../lib/constants';
import database from '../../lib/database'; import { useTheme } from '../../theme';
import { emojisByCategory } from './emojis'; import { IEmoji, ICustomEmojis } from '../../definitions';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import { useAppSelector, useFrequentlyUsedEmoji } from '../../lib/hooks';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import { addFrequentlyUsed } from '../../lib/methods';
import log from '../../lib/methods/helpers/log'; import { IEmojiPickerProps, EventTypes } from './interfaces';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
interface IEmojiPickerProps { const EmojiPicker = ({
isMessageContainsOnlyEmoji?: boolean; onItemClicked,
getCustomEmoji?: TGetCustomEmoji; isEmojiKeyboard = false,
baseUrl: string; searching = false,
customEmojis: ICustomEmojis; searchedEmojis = []
style?: StyleProp<ImageStyle>; }: IEmojiPickerProps): React.ReactElement | null => {
theme: TSupportedThemes; const { colors } = useTheme();
onEmojiSelected: (emoji: string, shortname?: string) => void; const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
tabEmojiStyle?: StyleProp<TextStyle>;
}
interface IEmojiPickerState { const allCustomEmojis: ICustomEmojis = useAppSelector(
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[]; state => state.customEmojis,
customEmojis: any; () => true
show: boolean; );
width: number | null; const customEmojis = Object.keys(allCustomEmojis)
} .filter(item => item === allCustomEmojis[item].name)
.map(item => ({
name: allCustomEmojis[item].name,
extension: allCustomEmojis[item].extension
}));
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> { const handleEmojiSelect = (emoji: IEmoji) => {
constructor(props: IEmojiPickerProps) { onItemClicked(EventTypes.EMOJI_PRESSED, emoji);
super(props); addFrequentlyUsed(emoji);
const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name)
.map(item => ({
content: props.customEmojis[item].name,
extension: props.customEmojis[item].extension,
isCustom: true
}));
this.state = {
frequentlyUsed: [],
customEmojis,
show: false,
width: null
};
}
async componentDidMount() {
await this.updateFrequentlyUsed();
this.setState({ show: true });
}
shouldComponentUpdate(nextProps: IEmojiPickerProps, nextState: IEmojiPickerState) {
const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
if (nextState.show !== show) {
return true;
}
if (nextState.width !== width) {
return true;
}
if (!dequal(nextState.frequentlyUsed, frequentlyUsed)) {
return true;
}
return false;
}
onEmojiSelected = (emoji: IEmoji) => {
try {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) {
this._addFrequentlyUsed({
content: emoji.content,
extension: emoji.extension,
isCustom: true
});
onEmojiSelected(`:${emoji.content}:`);
} else {
const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname);
}
} catch (e) {
log(e);
}
}; };
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => { const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel;
try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) {
// Do nothing
}
await db.write(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update(f => {
if (f.count) {
f.count += 1;
}
});
} else {
await freqEmojiCollection.create(f => {
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
Object.assign(f, emoji);
f.count = 1;
});
}
});
});
updateFrequentlyUsed = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
}
return shortnameToUnicode(`${item.content}`);
});
this.setState({ frequentlyUsed });
};
onLayout = ({
nativeEvent: {
layout: { width }
}
}: any) => this.setState({ width });
renderCategory(category: keyof typeof emojisByCategory, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props;
let emojis = []; let emojis = [];
if (i === 0) { if (i === 0) {
emojis = frequentlyUsed; emojis = frequentlyUsed;
@ -158,49 +47,40 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
} else { } else {
emojis = emojisByCategory[category]; emojis = emojisByCategory[category];
} }
return ( if (!emojis.length) {
<EmojiCategory
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer}
width={width}
baseUrl={baseUrl}
tabLabel={label}
/>
);
}
render() {
const { show, frequentlyUsed } = this.state;
const { tabEmojiStyle, theme } = this.props;
if (!show) {
return null; return null;
} }
return ( return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
<View onLayout={this.onLayout} style={{ flex: 1 }}> };
if (!loaded) {
return null;
}
return (
<View style={styles.emojiPickerContainer}>
{searching ? (
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
) : (
<ScrollableTabView <ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />} renderTabBar={() => <TabBar />}
contentProps={{ contentProps={{
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none' keyboardDismissMode: 'none'
}} }}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: colors.messageboxBackground }}
> >
{categories.tabs.map((tab: any, i) => {categories.tabs.map((tab: any, i) => renderCategory(tab.category, i, tab.tabLabel))}
i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab
: this.renderCategory(tab.category, i, tab.tabLabel)
)}
</ScrollableTabView> </ScrollableTabView>
</View> )}
); {isEmojiKeyboard && (
} <Footer
} onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
/>
)}
</View>
);
};
const mapStateToProps = (state: IApplicationState) => ({ export default EmojiPicker;
customEmojis: state.customEmojis,
baseUrl: state.share.server.server || state.server.server
});
export default connect(mapStateToProps)(EmojiPicker);

View File

@ -0,0 +1,26 @@
import { TIconsName } from '../CustomIcon';
import { IEmoji } from '../../definitions';
export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed',
SEARCH_PRESSED = 'searchPressed'
}
export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: IEmoji) => void;
isEmojiKeyboard?: boolean;
searching?: boolean;
searchedEmojis?: IEmoji[];
}
export interface IFooterProps {
onBackspacePressed: () => void;
onSearchPressed: () => void;
}
export interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: TIconsName[];
}

View File

@ -2,20 +2,23 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
export const EMOJI_BUTTON_SIZE = 44;
export const EMOJI_SIZE = EMOJI_BUTTON_SIZE - 16;
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
flex: 1 flex: 1
}, },
tabsContainer: { tabsContainer: {
height: 45, height: EMOJI_BUTTON_SIZE,
flexDirection: 'row', flexDirection: 'row'
paddingTop: 5
}, },
tab: { tab: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingBottom: 10 paddingVertical: 10,
width: EMOJI_BUTTON_SIZE
}, },
tabEmoji: { tabEmoji: {
fontSize: 20, fontSize: 20,
@ -33,7 +36,6 @@ export default StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
height: 2, height: 2,
backgroundColor: 'rgba(0,0,0,0.05)',
bottom: 0 bottom: 0
}, },
categoryContainer: { categoryContainer: {
@ -49,10 +51,34 @@ export default StyleSheet.create({
}, },
categoryEmoji: { categoryEmoji: {
...sharedStyles.textAlignCenter, ...sharedStyles.textAlignCenter,
textAlignVertical: 'center',
fontSize: EMOJI_SIZE,
backgroundColor: 'transparent', backgroundColor: 'transparent',
color: '#ffffff' color: '#ffffff'
}, },
customCategoryEmoji: { customCategoryEmoji: {
margin: 8 height: EMOJI_SIZE,
} width: EMOJI_SIZE
},
emojiButton: {
alignItems: 'center',
justifyContent: 'center',
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE
},
footerContainer: {
height: EMOJI_BUTTON_SIZE,
paddingHorizontal: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderTopWidth: 1
},
footerButtonsContainer: {
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE,
justifyContent: 'center',
alignItems: 'center'
},
emojiPickerContainer: { flex: 1 }
}); });

View File

@ -9,7 +9,7 @@ import { PADDING_HORIZONTAL } from './constants';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingBottom: 12, paddingVertical: 8,
paddingHorizontal: PADDING_HORIZONTAL paddingHorizontal: PADDING_HORIZONTAL
}, },
title: { title: {

View File

@ -5,7 +5,7 @@ import { Header } from '.';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
marginVertical: 16 marginBottom: 16
} }
}); });

View File

@ -2,6 +2,6 @@ import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
contentContainerStyleFlatList: { contentContainerStyleFlatList: {
paddingVertical: 32 paddingVertical: 16
} }
}); });

View File

@ -4,7 +4,7 @@ import sharedStyles from '../../views/Styles';
export const BUTTON_HEIGHT = 48; export const BUTTON_HEIGHT = 48;
export const SERVICE_HEIGHT = 58; export const SERVICE_HEIGHT = 58;
export const BORDER_RADIUS = 2; export const BORDER_RADIUS = 4;
export const SERVICES_COLLAPSED_HEIGHT = 174; export const SERVICES_COLLAPSED_HEIGHT = 174;
export default StyleSheet.create({ export default StyleSheet.create({

View File

@ -1,32 +1,29 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native'; import { FlatList, StyleSheet, Text, View } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme'; import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { TAnyMessageModel, TFrequentlyUsedEmojiModel } from '../../definitions'; import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch'; import Touch from '../Touch';
type TItem = TFrequentlyUsedEmojiModel | string;
export interface IHeader { export interface IHeader {
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void; handleReaction: (emoji: IEmoji, message: TAnyMessageModel) => void;
server: string;
message: TAnyMessageModel; message: TAnyMessageModel;
isMasterDetail: boolean; isMasterDetail: boolean;
} }
type TOnReaction = ({ emoji }: { emoji: TItem }) => void; type TOnReaction = ({ emoji }: { emoji: IEmoji }) => void;
interface THeaderItem { interface THeaderItem {
item: TItem; item: IEmoji;
onReaction: TOnReaction; onReaction: TOnReaction;
server: string;
theme: TSupportedThemes; theme: TSupportedThemes;
} }
@ -64,30 +61,19 @@ const styles = StyleSheet.create({
} }
}); });
const keyExtractor = (item: TItem) => { const HeaderItem = ({ item, onReaction, theme }: THeaderItem) => (
const emojiModel = item as TFrequentlyUsedEmojiModel; <Touch
return (emojiModel.id ? emojiModel.content : item) as string; testID={`message-actions-emoji-${item}`}
}; onPress={() => onReaction({ emoji: item })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley']; >
{typeof item === 'string' ? (
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => { <Text style={styles.headerIcon}>{shortnameToUnicode(`:${item}:`)}</Text>
const emojiModel = item as TFrequentlyUsedEmojiModel; ) : (
const emoji = (emojiModel.id ? emojiModel.content : item) as string; <CustomEmoji style={styles.customEmoji} emoji={item} />
return ( )}
<Touch </Touch>
testID={`message-actions-emoji-${emoji}`} );
onPress={() => onReaction({ emoji: `:${emoji}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
>
{emojiModel?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={emojiModel} baseUrl={server} />
) : (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)}
</Touch>
);
};
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => ( const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Touch <Touch
@ -99,49 +85,35 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
</Touch> </Touch>
); );
const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => { const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<TItem[]>([]);
const { width, height } = useDimensions(); const { width, height } = useDimensions();
const { theme } = useTheme(); const { theme } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1);
// TODO: create custom hook to re-render based on screen size const onReaction: TOnReaction = ({ emoji }) => {
const setEmojis = async () => { handleReaction(emoji, message);
try { addFrequentlyUsed(emoji);
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis);
} catch {
// Do nothing
}
}; };
useEffect(() => { const renderItem = ({ item }: { item: IEmoji }) => <HeaderItem item={item} onReaction={onReaction} theme={theme} />;
setEmojis();
}, []);
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = ({ item }: { item: TItem }) => (
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
);
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />; const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
if (!loaded) {
return null;
}
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList <FlatList
data={items} data={frequentlyUsed.slice(0, quantity)}
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor} keyExtractor={item => (typeof item === 'string' ? item : item.name)}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
scrollEnabled={false} scrollEnabled={false}
horizontal horizontal

View File

@ -12,12 +12,12 @@ import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../lib/methods/helpers/events'; import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet'; import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header'; import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events'; import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods'; import { getPermalinkMessage } from '../../lib/methods';
import { hasPermission } from '../../lib/methods/helpers'; import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
export interface IMessageActionsProps { export interface IMessageActionsProps {
@ -26,7 +26,7 @@ export interface IMessageActionsProps {
user: Pick<ILoggedUser, 'id'>; user: Pick<ILoggedUser, 'id'>;
editInit: (message: TAnyMessageModel) => void; editInit: (message: TAnyMessageModel) => void;
reactionInit: (message: TAnyMessageModel) => void; reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: string, messageId: string) => void; onReactionPress: (shortname: IEmoji, messageId: string) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void; replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean; isMasterDetail: boolean;
isReadOnly: boolean; isReadOnly: boolean;
@ -37,11 +37,12 @@ export interface IMessageActionsProps {
Message_AllowPinning?: boolean; Message_AllowPinning?: boolean;
Message_AllowStarring?: boolean; Message_AllowStarring?: boolean;
Message_Read_Receipt_Store_Users?: boolean; Message_Read_Receipt_Store_Users?: boolean;
server: string;
editMessagePermission?: string[]; editMessagePermission?: string[];
deleteMessagePermission?: string[]; deleteMessagePermission?: string[];
forceDeleteMessagePermission?: string[]; forceDeleteMessagePermission?: string[];
deleteOwnMessagePermission?: string[];
pinMessagePermission?: string[]; pinMessagePermission?: string[];
createDirectMessagePermission?: string[];
} }
export interface IMessageActions { export interface IMessageActions {
@ -60,7 +61,6 @@ const MessageActions = React.memo(
onReactionPress, onReactionPress,
replyInit, replyInit,
isReadOnly, isReadOnly,
server,
Message_AllowDeleting, Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing, Message_AllowEditing,
@ -72,7 +72,9 @@ const MessageActions = React.memo(
editMessagePermission, editMessagePermission,
deleteMessagePermission, deleteMessagePermission,
forceDeleteMessagePermission, forceDeleteMessagePermission,
pinMessagePermission deleteOwnMessagePermission,
pinMessagePermission,
createDirectMessagePermission
}, },
ref ref
) => { ) => {
@ -80,19 +82,27 @@ const MessageActions = React.memo(
hasEditPermission: false, hasEditPermission: false,
hasDeletePermission: false, hasDeletePermission: false,
hasForceDeletePermission: false, hasForceDeletePermission: false,
hasPinPermission: false hasPinPermission: false,
hasDeleteOwnPermission: false
}; };
const { showActionSheet, hideActionSheet } = useActionSheet(); const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async () => { const getPermissions = async () => {
try { try {
const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission]; const permission = [
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission,
deleteOwnMessagePermission
];
const result = await hasPermission(permission, room.rid); const result = await hasPermission(permission, room.rid);
permissions = { permissions = {
hasEditPermission: result[0], hasEditPermission: result[0],
hasDeletePermission: result[1], hasDeletePermission: result[1],
hasForceDeletePermission: result[2], hasForceDeletePermission: result[2],
hasPinPermission: result[3] hasPinPermission: result[3],
hasDeleteOwnPermission: result[4]
}; };
} catch { } catch {
// Do nothing // Do nothing
@ -134,7 +144,7 @@ const MessageActions = React.memo(
if (tmid === message.id) { if (tmid === message.id) {
return false; return false;
} }
const deleteOwn = isOwn(message); const deleteOwn = isOwn(message) && permissions.hasDeleteOwnPermission;
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) { if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false; return false;
} }
@ -237,6 +247,23 @@ const MessageActions = React.memo(
replyInit(message, false); replyInit(message, false);
}; };
const handleReplyInDM = async (message: TAnyMessageModel) => {
if (message?.u?.username) {
const result = await Services.createDirectMessage(message.u.username);
if (result.success) {
const { room } = result;
const params = {
rid: room.rid,
name: getRoomTitle(room),
t: room.t,
roomUserId: getUidDirectMessage(room),
replyInDM: message
};
Navigation.replace('RoomView', params);
}
}
};
const handleStar = async (message: TAnyMessageModel) => { const handleStar = async (message: TAnyMessageModel) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR); logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try { try {
@ -261,12 +288,10 @@ const MessageActions = React.memo(
const handleReaction: IHeader['handleReaction'] = (shortname, message) => { const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION); logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) { if (shortname) {
// TODO: evaluate unification with IEmoji onReactionPress(shortname, message.id);
onReactionPress(shortname as any, message.id);
} else { } else {
reactionInit(message); setTimeout(() => reactionInit(message), ACTION_SHEET_ANIMATION_DURATION);
} }
// close actionSheet when click at header
hideActionSheet(); hideActionSheet();
}; };
@ -327,18 +352,7 @@ const MessageActions = React.memo(
}; };
const getOptions = (message: TAnyMessageModel) => { const getOptions = (message: TAnyMessageModel) => {
let options: TActionSheetOptionsItem[] = []; const options: TActionSheetOptionsItem[] = [];
// Reply
if (!isReadOnly && !tmid) {
options = [
{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}
];
}
// Quote // Quote
if (!isReadOnly) { if (!isReadOnly) {
@ -349,21 +363,23 @@ const MessageActions = React.memo(
}); });
} }
// Edit // Reply
if (allowEdit(message)) { if (!isReadOnly && !tmid) {
options.push({ options.push({
title: I18n.t('Edit'), title: I18n.t('Reply_in_Thread'),
icon: 'edit', icon: 'threads',
onPress: () => handleEdit(message) onPress: () => handleReply(message)
}); });
} }
// Permalink // Reply in DM
options.push({ if (room.t !== 'd' && room.t !== 'l' && createDirectMessagePermission) {
title: I18n.t('Permalink'), options.push({
icon: 'link', title: I18n.t('Reply_in_direct_message'),
onPress: () => handlePermalink(message) icon: 'arrow-back',
}); onPress: () => handleReplyInDM(message)
});
}
// Create Discussion // Create Discussion
options.push({ options.push({
@ -372,14 +388,12 @@ const MessageActions = React.memo(
onPress: () => handleCreateDiscussion(message) onPress: () => handleCreateDiscussion(message)
}); });
// Mark as unread // Permalink
if (message.u && message.u._id !== user.id) { options.push({
options.push({ title: I18n.t('Get_link'),
title: I18n.t('Mark_unread'), icon: 'link',
icon: 'flag', onPress: () => handlePermalink(message)
onPress: () => handleUnread(message) });
});
}
// Copy // Copy
options.push({ options.push({
@ -395,12 +409,12 @@ const MessageActions = React.memo(
onPress: () => handleShare(message) onPress: () => handleShare(message)
}); });
// Star // Edit
if (Message_AllowStarring) { if (allowEdit(message)) {
options.push({ options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'), title: I18n.t('Edit'),
icon: message.starred ? 'star-filled' : 'star', icon: 'edit',
onPress: () => handleStar(message) onPress: () => handleEdit(message)
}); });
} }
@ -413,6 +427,24 @@ const MessageActions = React.memo(
}); });
} }
// Star
if (Message_AllowStarring) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
onPress: () => handleStar(message)
});
}
// Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({
title: I18n.t('Mark_unread'),
icon: 'flag',
onPress: () => handleUnread(message)
});
}
// Read Receipts // Read Receipts
if (Message_Read_Receipt_Store_Users) { if (Message_Read_Receipt_Store_Users) {
options.push({ options.push({
@ -460,7 +492,7 @@ const MessageActions = React.memo(
headerHeight: HEADER_HEIGHT, headerHeight: HEADER_HEIGHT,
customHeader: customHeader:
!isReadOnly || room.reactWhenReadOnly ? ( !isReadOnly || room.reactWhenReadOnly ? (
<Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} /> <Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
) : null ) : null
}); });
}; };
@ -483,8 +515,10 @@ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'], editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'], deleteMessagePermission: state.permissions['delete-message'],
deleteOwnMessagePermission: state.permissions['delete-own-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'], forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message'] pinMessagePermission: state.permissions['pin-message'],
createDirectMessagePermission: state.permissions['create-d']
}); });
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions); export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -6,22 +6,31 @@ import { Provider } from 'react-redux';
import store from '../../lib/store'; import store from '../../lib/store';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { themes } from '../../lib/constants'; import { ThemeContext, TSupportedThemes } from '../../theme';
import { TSupportedThemes } from '../../theme'; import { EventTypes } from '../EmojiPicker/interfaces';
import { IEmoji } from '../../definitions';
import { colors } from '../../lib/constants';
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => { const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => {
const onEmojiSelected = (emoji: string) => { const onItemClicked = (eventType: EventTypes, emoji?: IEmoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
}; };
return ( return (
<Provider store={store}> <Provider store={store}>
<View <ThemeContext.Provider
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} value={{
testID='messagebox-keyboard-emoji' theme,
colors: colors[theme]
}}
> >
<EmojiPicker onEmojiSelected={onEmojiSelected} theme={theme} /> <View
</View> style={[styles.emojiKeyboardContainer, { borderTopColor: colors[theme].borderColor }]}
testID='messagebox-keyboard-emoji'
>
<EmojiPicker onItemClicked={onItemClicked} isEmojiKeyboard={true} />
</View>
</ThemeContext.Provider>
</Provider> </Provider>
); );
}; };

View File

@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon';
import { IEmoji } from '../../definitions';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import { addFrequentlyUsed, searchEmojis } from '../../lib/methods';
import { useDebounce } from '../../lib/methods/helpers';
import sharedStyles from '../../views/Styles';
import { PressableEmoji } from '../EmojiPicker/PressableEmoji';
import { EmojiSearch } from '../EmojiPicker/EmojiSearch';
import { EMOJI_BUTTON_SIZE } from '../EmojiPicker/styles';
import { events, logEvent } from '../../lib/methods/helpers/log';
const BUTTON_HIT_SLOP = { top: 4, right: 4, bottom: 4, left: 4 };
const styles = StyleSheet.create({
listContainer: {
height: EMOJI_BUTTON_SIZE,
margin: 8,
flexGrow: 1
},
container: {
borderTopWidth: 1
},
searchContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
marginBottom: 12
},
backButton: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
emptyText: {
...sharedStyles.textRegular,
fontSize: 16
},
inputContainer: {
flex: 1
}
});
interface IEmojiSearchBarProps {
openEmoji: () => void;
closeEmoji: () => void;
onEmojiSelected: (emoji: IEmoji) => void;
}
const EmojiSearchBar = ({ openEmoji, closeEmoji, onEmojiSelected }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const { frequentlyUsed } = useFrequentlyUsedEmoji(true);
const [emojis, setEmojis] = useState<IEmoji[]>([]);
const handleTextChange = useDebounce(async (text: string) => {
logEvent(events.MB_SB_EMOJI_SEARCH);
setSearchText(text);
const result = await searchEmojis(text);
setEmojis(result);
}, 300);
const handleEmojiSelected = (emoji: IEmoji) => {
logEvent(events.MB_SB_EMOJI_SELECTED);
onEmojiSelected(emoji);
addFrequentlyUsed(emoji);
};
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={handleEmojiSelected} />;
return (
<View style={[styles.container, { borderTopColor: colors.borderColor, backgroundColor: colors.messageboxBackground }]}>
<FlatList
horizontal
data={searchText ? emojis : frequentlyUsed}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
ListEmptyComponent={() => (
<View style={styles.emptyContainer} testID='no-results-found'>
<Text style={[styles.emptyText, { color: colors.auxiliaryText }]}>{I18n.t('No_results_found')}</Text>
</View>
)}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
contentContainerStyle={styles.listContainer}
keyboardShouldPersistTaps='always'
/>
<View style={styles.searchContainer}>
<Pressable
style={({ pressed }: { pressed: boolean }) => [styles.backButton, { opacity: pressed ? 0.7 : 1 }]}
onPress={openEmoji}
hitSlop={BUTTON_HIT_SLOP}
testID='openback-emoji-keyboard'
>
<CustomIcon name='chevron-left' size={24} color={colors.auxiliaryTintColor} />
</Pressable>
<View style={styles.inputContainer}>
<EmojiSearch onBlur={closeEmoji} onChangeText={handleTextChange} />
</View>
</View>
</View>
);
};
export default EmojiSearchBar;

View File

@ -1,10 +1,9 @@
import React, { useContext } from 'react'; import React from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MessageboxContext from '../Context';
import styles from '../styles'; import styles from '../styles';
interface IMessageBoxMentionEmoji { interface IMessageBoxMentionEmoji {
@ -12,13 +11,10 @@ interface IMessageBoxMentionEmoji {
} }
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => { const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
const context = useContext(MessageboxContext); if (typeof item === 'string') {
const { baseUrl } = context; return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
if (item.name) {
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
} }
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>; return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} />;
}; };
export default MentionEmoji; export default MentionEmoji;

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Text, TouchableOpacity } from 'react-native'; import { Text, TouchableOpacity, View } from 'react-native';
import { themes } from '../../../lib/constants'; import { themes } from '../../../lib/constants';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
@ -37,7 +37,9 @@ const MentionItemContent = React.memo(({ trackingType, item }: IMessageBoxMentio
case MENTIONS_TRACKING_TYPE_COMMANDS: case MENTIONS_TRACKING_TYPE_COMMANDS:
return ( return (
<> <>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text> <View style={[styles.slash, { backgroundColor: themes[theme].borderColor }]}>
<Text style={{ color: themes[theme].tintColor }}>/</Text>
</View>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</> </>
); );

View File

@ -29,7 +29,8 @@ const styles = StyleSheet.create({
}, },
username: { username: {
fontSize: 16, fontSize: 16,
...sharedStyles.textMedium ...sharedStyles.textMedium,
flexShrink: 1
}, },
time: { time: {
fontSize: 12, fontSize: 12,
@ -67,7 +68,7 @@ const ReplyPreview = React.memo(
<View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}>
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}> <View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
<View style={styles.header}> <View style={styles.header}>
<Text style={[styles.username, { color: themes[theme].tintColor }]}> <Text numberOfLines={1} style={[styles.username, { color: themes[theme].tintColor }]}>
{useRealName ? message.u?.name : message.u?.username} {useRealName ? message.u?.name : message.u?.username}
</Text> </Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>

View File

@ -4,5 +4,6 @@ export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_TRACKING_TYPE_ROOMS = '#'; export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
export const MENTIONS_TRACKING_TYPE_CANNED = '!'; export const MENTIONS_TRACKING_TYPE_CANNED = '!';
export const MENTIONS_COUNT_TO_DISPLAY = 4; export const MENTIONS_COUNT_TO_DISPLAY = 4;
export const MAX_EMOJIS_TO_DISPLAY = 20;
export const TIMEOUT_CLOSE_EMOJI = 300; export const TIMEOUT_CLOSE_EMOJI = 300;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View } from 'react-native'; import { Alert, Keyboard, NativeModules, Text, View, BackHandler } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard'; import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker'; import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
@ -13,12 +13,11 @@ import { TextInput, IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles'; import styles from './styles';
import database from '../../lib/database'; import database from '../../lib/database';
import { emojis } from '../EmojiPicker/emojis';
import log, { events, logEvent } from '../../lib/methods/helpers/log'; import log, { events, logEvent } from '../../lib/methods/helpers/log';
import RecordAudio from './RecordAudio'; import RecordAudio from './RecordAudio';
import I18n from '../../i18n'; import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview'; import ReplyPreview from './ReplyPreview';
import { themes } from '../../lib/constants'; import { themes, emojis } from '../../lib/constants';
import LeftButtons from './LeftButtons'; import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons'; import RightButtons from './RightButtons';
import { canUploadFile } from '../../lib/methods/helpers/media'; import { canUploadFile } from '../../lib/methods/helpers/media';
@ -51,7 +50,8 @@ import {
TGetCustomEmoji, TGetCustomEmoji,
TSubscriptionModel, TSubscriptionModel,
TThreadModel, TThreadModel,
IMessage IMessage,
IEmoji
} from '../../definitions'; } from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
@ -59,6 +59,9 @@ import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersi
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types'; import { ChatsStackParamList } from '../../stacks/types';
import { EventTypes } from '../EmojiPicker/interfaces';
import EmojiSearchbar from './EmojiSearchbar';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -129,6 +132,7 @@ interface IMessageBoxState {
tshow: boolean; tshow: boolean;
mentionLoading: boolean; mentionLoading: boolean;
permissionToUpload: boolean; permissionToUpload: boolean;
showEmojiSearchbar: boolean;
} }
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -183,7 +187,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
command: {}, command: {},
tshow: this.sendThreadToChannel, tshow: this.sendThreadToChannel,
mentionLoading: false, mentionLoading: false,
permissionToUpload: true permissionToUpload: true,
showEmojiSearchbar: false
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
@ -209,6 +214,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
...videoPickerConfig, ...videoPickerConfig,
...libPickerLabels ...libPickerLabels
}; };
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
} }
get sendThreadToChannel() { get sendThreadToChannel() {
@ -326,7 +333,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tshow, tshow,
mentionLoading, mentionLoading,
trackingType, trackingType,
permissionToUpload permissionToUpload,
showEmojiSearchbar
} = this.state; } = this.state;
const { const {
@ -346,6 +354,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) { if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true; return true;
} }
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!isFocused()) { if (!isFocused()) {
return false; return false;
} }
@ -438,6 +449,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (isTablet) { if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands); EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
} }
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
} }
setOptions = async () => { setOptions = async () => {
@ -519,7 +531,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 100); }, 100);
onKeyboardResigned = () => { onKeyboardResigned = () => {
this.closeEmoji(); const { showEmojiSearchbar } = this.state;
if (!showEmojiSearchbar) {
this.closeEmoji();
}
}; };
onPressMention = (item: any) => { onPressMention = (item: any) => {
@ -577,18 +592,50 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
onEmojiSelected = (keyboardId: string, params: { emoji: string }) => { onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: IEmoji }) => {
const { eventType, emoji } = params;
const { text } = this; const { text } = this;
const { emoji } = params;
let newText = ''; let newText = '';
// if messagebox has an active cursor // if messagebox has an active cursor
const { start, end } = this.selection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`; let newCursor;
const newCursor = cursor + emoji.length;
this.setInput(newText, { start: newCursor, end: newCursor }); switch (eventType) {
this.setShowSend(true); case EventTypes.BACKSPACE_PRESSED:
logEvent(events.MB_BACKSPACE);
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
let charsToRemove = 1;
const lastEmoji = text.substr(cursor > 0 ? cursor - 2 : text.length - 2, cursor > 0 ? cursor : text.length);
// Check if last character is an emoji
if (emojiRegex.test(lastEmoji)) charsToRemove = 2;
newText =
text.substr(0, (cursor > 0 ? cursor : text.length) - charsToRemove) + text.substr(cursor > 0 ? cursor : text.length);
newCursor = cursor - charsToRemove;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(newText !== '');
break;
case EventTypes.EMOJI_PRESSED:
logEvent(events.MB_EMOJI_SELECTED);
let emojiText = '';
if (typeof emoji === 'string') {
const shortname = `:${emoji}:`;
emojiText = shortnameToUnicode(shortname);
} else {
emojiText = `:${emoji.name}:`;
}
newText = `${text.substr(0, cursor)}${emojiText}${text.substr(cursor)}`;
newCursor = cursor + emojiText.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
break;
case EventTypes.SEARCH_PRESSED:
logEvent(events.MB_EMOJI_SEARCH_PRESSED);
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true });
break;
default:
// Do nothing
}
}; };
getPermalink = async (message: any) => { getPermalink = async (message: any) => {
@ -611,7 +658,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
getUsers = debounce(async (keyword: any) => { getUsers = debounce(async (keyword: any) => {
let res = await search({ text: keyword, filterRooms: false, filterUsers: true }); const { rid } = this.props;
let res = await search({ text: keyword, filterRooms: false, filterUsers: true, rid });
res = [...this.getFixedMentions(keyword), ...res]; res = [...this.getFixedMentions(keyword), ...res];
this.setState({ mentions: res, mentionLoading: false }); this.setState({ mentions: res, mentionLoading: false });
}, 300); }, 300);
@ -621,16 +669,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.setState({ mentions: res, mentionLoading: false }); this.setState({ mentions: res, mentionLoading: false });
}, 300); }, 300);
getEmojis = debounce(async (keyword: any) => { getCustomEmojis = async (keyword: any, count: number) => {
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
const likeString = sanitizeLikeString(keyword); const likeString = sanitizeLikeString(keyword);
const whereClause = []; const whereClause = [];
if (likeString) { if (likeString) {
whereClause.push(Q.where('name', Q.like(`${likeString}%`))); whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
} }
let customEmojis = await customEmojisCollection.query(...whereClause).fetch(); const db = database.active;
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY); const customEmojisCollection = db.get('custom_emojis');
const customEmojis = await (await customEmojisCollection.query(...whereClause).fetch()).slice(0, count);
return customEmojis;
};
getEmojis = debounce(async (keyword: any) => {
const customEmojis = await this.getCustomEmojis(keyword, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY); const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY); const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [], mentionLoading: false }); this.setState({ mentions: mergedEmojis || [], mentionLoading: false });
@ -881,7 +933,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
openEmoji = () => { openEmoji = () => {
logEvent(events.ROOM_OPEN_EMOJI); logEvent(events.ROOM_OPEN_EMOJI);
this.setState({ showEmojiKeyboard: true }); this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
this.stopTrackingMention();
}; };
recordingCallback = (recording: any) => { recordingCallback = (recording: any) => {
@ -903,7 +956,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
closeEmoji = () => { closeEmoji = () => {
this.setState({ showEmojiKeyboard: false }); this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: false });
};
closeEmojiKeyboardAndFocus = () => {
logEvent(events.ROOM_CLOSE_EMOJI);
this.closeEmoji();
this.focus();
}; };
closeEmojiAndAction = (action?: Function, params?: any) => { closeEmojiAndAction = (action?: Function, params?: any) => {
@ -926,7 +985,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.clearInput(); this.clearInput();
this.debouncedOnChangeText.stop(); this.debouncedOnChangeText.stop();
this.closeEmoji(); this.closeEmojiKeyboardAndFocus();
this.stopTrackingMention(); this.stopTrackingMention();
this.handleTyping(false); this.handleTyping(false);
if (message.trim() === '' && !showSend) { if (message.trim() === '' && !showSend) {
@ -1083,10 +1142,34 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
); );
}; };
renderEmojiSearchbar = () => {
const { showEmojiSearchbar } = this.state;
return showEmojiSearchbar ? (
<EmojiSearchbar
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
onEmojiSelected={(emoji: IEmoji) => {
this.onKeyboardItemSelected('EmojiKeyboard', { eventType: EventTypes.EMOJI_PRESSED, emoji });
}}
/>
) : null;
};
handleBackPress = () => {
const { showEmojiSearchbar } = this.state;
if (showEmojiSearchbar) {
this.setState({ showEmojiSearchbar: false });
return true;
}
return false;
};
renderContent = () => { renderContent = () => {
const { const {
recording, recording,
showEmojiKeyboard, showEmojiKeyboard,
showEmojiSearchbar,
showSend, showSend,
mentions, mentions,
trackingType, trackingType,
@ -1149,11 +1232,11 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const textInputAndButtons = !recording ? ( const textInputAndButtons = !recording ? (
<> <>
<LeftButtons <LeftButtons
showEmojiKeyboard={showEmojiKeyboard} showEmojiKeyboard={showEmojiKeyboard || showEmojiSearchbar}
editing={editing} editing={editing}
editCancel={this.editCancel} editCancel={this.editCancel}
openEmoji={this.openEmoji} openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji} closeEmoji={this.closeEmojiKeyboardAndFocus}
/> />
<TextInput <TextInput
ref={component => (this.component = component)} ref={component => (this.component = component)}
@ -1197,6 +1280,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
{recordAudio} {recordAudio}
</View> </View>
{this.renderSendToChannel()} {this.renderSendToChannel()}
{this.renderEmojiSearchbar()}
</View> </View>
{children} {children}
</> </>
@ -1224,7 +1308,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null} kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbInitialProps={{ theme }} kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned} onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onEmojiSelected} onItemSelected={this.onKeyboardItemSelected}
trackInteractive trackInteractive
requiresSameParentToManageScrollView requiresSameParentToManageScrollView
addBottomView addBottomView

View File

@ -105,7 +105,7 @@ export default StyleSheet.create({
}, },
emojiKeyboardContainer: { emojiKeyboardContainer: {
flex: 1, flex: 1,
borderTopWidth: StyleSheet.hairlineWidth borderTopWidth: 1
}, },
slash: { slash: {
height: 30, height: 30,
@ -113,7 +113,7 @@ export default StyleSheet.create({
padding: 5, padding: 5,
paddingHorizontal: 12, paddingHorizontal: 12,
marginHorizontal: 10, marginHorizontal: 10,
borderRadius: 2 borderRadius: 4
}, },
commandPreviewImage: { commandPreviewImage: {
justifyContent: 'center', justifyContent: 'center',

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; import { Text, StyleProp, ViewStyle } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../lib/constants'; import { themes } from '../../../lib/constants';
@ -12,16 +12,17 @@ interface IPasscodeButton {
icon?: TIconsName; icon?: TIconsName;
disabled?: boolean; disabled?: boolean;
onPress?: Function; onPress?: Function;
style?: StyleProp<ViewStyle>;
} }
const Button = React.memo(({ text, disabled, onPress, icon }: IPasscodeButton) => { const Button = React.memo(({ style, text, disabled, onPress, icon }: IPasscodeButton) => {
const { theme } = useTheme(); const { theme } = useTheme();
const press = () => onPress && onPress(text); const press = () => onPress && onPress(text);
return ( return (
<Touch <Touch
style={[styles.buttonView, { backgroundColor: 'transparent' }]} style={[styles.buttonView, { backgroundColor: 'transparent' }, style]}
underlayColor={themes[theme].passcodeButtonActive} underlayColor={themes[theme].passcodeButtonActive}
rippleColor={themes[theme].passcodeButtonActive} rippleColor={themes[theme].passcodeButtonActive}
enabled={!disabled} enabled={!disabled}

View File

@ -1,9 +1,10 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
import { Col, Grid, Row } from 'react-native-easy-grid'; import { Col, Grid, Row } from 'react-native-easy-grid';
import range from 'lodash/range'; import range from 'lodash/range';
import { View } from 'react-native'; import { View } from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import Orientation from 'react-native-orientation-locker';
import styles from './styles'; import styles from './styles';
import Button from './Button'; import Button from './Button';
@ -14,6 +15,8 @@ import { useTheme } from '../../../theme';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
import Title from './Title'; import Title from './Title';
import Subtitle from './Subtitle'; import Subtitle from './Subtitle';
import { useDimensions } from '../../../dimensions';
import { isTablet } from '../../../lib/methods/helpers';
interface IPasscodeBase { interface IPasscodeBase {
type: string; type: string;
@ -34,7 +37,25 @@ export interface IBase {
const Base = forwardRef<IBase, IPasscodeBase>( const Base = forwardRef<IBase, IPasscodeBase>(
({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => { ({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => {
useLayoutEffect(() => {
if (!isTablet) {
Orientation.lockToPortrait();
}
return () => {
if (!isTablet) {
Orientation.unlockAllOrientations();
}
};
}, []);
const { theme } = useTheme(); const { theme } = useTheme();
const { height } = useDimensions();
// 206 is the height of the header calculating the margins, icon size height, title font size and subtitle height.
// 56 is a fixed number to decrease the height of button numbers.
const dinamicHeight = (height - 206 - 56) / 4;
const heightButtonRow = { height: dinamicHeight > 102 ? 102 : dinamicHeight };
const rootRef = useRef<Animatable.View & View>(null); const rootRef = useRef<Animatable.View & View>(null);
const dotsRef = useRef<Animatable.View & View>(null); const dotsRef = useRef<Animatable.View & View>(null);
@ -103,40 +124,40 @@ const Base = forwardRef<IBase, IPasscodeBase>(
<Dots passcode={passcode} length={PASSCODE_LENGTH} /> <Dots passcode={passcode} length={PASSCODE_LENGTH} />
</Animatable.View> </Animatable.View>
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, heightButtonRow]}>
{range(1, 4).map(i => ( {range(1, 4).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={[styles.colButton, heightButtonRow]}>
<Button text={i.toString()} onPress={onPressNumber} /> <Button style={heightButtonRow} text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, heightButtonRow]}>
{range(4, 7).map(i => ( {range(4, 7).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={[styles.colButton, heightButtonRow]}>
<Button text={i.toString()} onPress={onPressNumber} /> <Button style={heightButtonRow} text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, heightButtonRow]}>
{range(7, 10).map(i => ( {range(7, 10).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={[styles.colButton, heightButtonRow]}>
<Button text={i.toString()} onPress={onPressNumber} /> <Button style={heightButtonRow} text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, heightButtonRow]}>
{showBiometry ? ( {showBiometry ? (
<Col style={styles.colButton}> <Col style={[styles.colButton, heightButtonRow]}>
<Button icon='fingerprint' onPress={onBiometryPress} /> <Button style={heightButtonRow} icon='fingerprint' onPress={onBiometryPress} />
</Col> </Col>
) : ( ) : (
<Col style={styles.colButton} /> <Col style={[styles.colButton, heightButtonRow]} />
)} )}
<Col style={styles.colButton}> <Col style={[styles.colButton, heightButtonRow]}>
<Button text='0' onPress={onPressNumber} /> <Button style={heightButtonRow} text='0' onPress={onPressNumber} />
</Col> </Col>
<Col style={styles.colButton}> <Col style={[styles.colButton, heightButtonRow]}>
<Button icon='backspace' onPress={onPressDelete} /> <Button style={heightButtonRow} icon='backspace' onPress={onPressDelete} />
</Col> </Col>
</Row> </Row>
</Grid> </Grid>

View File

@ -18,16 +18,12 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
}, },
buttonRow: {
height: 102
},
colButton: { colButton: {
flex: 0, flex: 0,
marginLeft: 12, marginLeft: 12,
marginRight: 12, marginRight: 12,
alignItems: 'center', alignItems: 'center',
width: 78, width: 78
height: 78
}, },
buttonText: { buttonText: {
fontSize: 28, fontSize: 28,
@ -37,7 +33,6 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 78, width: 78,
height: 78,
borderRadius: 4 borderRadius: 4
}, },
textTitle: { textTitle: {

View File

@ -1,145 +0,0 @@
import React from 'react';
import { StyleSheet, Text, Pressable, View, ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { FlatList } from 'react-native-gesture-handler';
import Emoji from './message/Emoji';
import { useTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji';
import { IReaction } from '../definitions';
import Avatar from './Avatar';
import sharedStyles from '../views/Styles';
const MIN_TAB_WIDTH = 70;
const styles = StyleSheet.create({
reactionsListContainer: { height: '100%', width: '100%' },
tabBarItem: {
paddingHorizontal: 10,
paddingBottom: 10,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
reactionCount: { marginLeft: 5 },
emojiName: { margin: 10 },
userItemContainer: { marginHorizontal: 10, marginVertical: 5, flexDirection: 'row' },
usernameContainer: { marginHorizontal: 10, justifyContent: 'center' },
usernameText: { fontSize: 17, ...sharedStyles.textMedium },
standardEmojiStyle: { fontSize: 20, color: '#fff' },
customEmojiStyle: { width: 25, height: 25 }
});
interface IReactionsListBase {
baseUrl: string;
getCustomEmoji: TGetCustomEmoji;
}
interface IReactionsListProps extends IReactionsListBase {
reactions?: IReaction[];
width: number;
}
interface ITabBarItem extends IReactionsListBase {
tab: IReaction;
index: number;
goToPage?: (index: number) => void;
}
interface IReactionsTabBar extends IReactionsListBase {
activeTab?: number;
tabs?: IReaction[];
goToPage?: (index: number) => void;
width: number;
}
const TabBarItem = ({ tab, index, goToPage, baseUrl, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme();
return (
<Pressable
key={tab.emoji}
onPress={() => {
goToPage?.(index);
}}
style={({ pressed }: { pressed: boolean }) => ({
opacity: pressed ? 0.7 : 1
})}
>
<View style={styles.tabBarItem}>
<Emoji
content={tab.emoji}
standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
</View>
</Pressable>
);
};
const ReactionsTabBar = ({ tabs, activeTab, goToPage, baseUrl, getCustomEmoji, width }: IReactionsTabBar) => {
const tabWidth = tabs && Math.max(width / tabs.length, MIN_TAB_WIDTH);
const { colors } = useTheme();
return (
<View>
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
{tabs?.map((tab, index) => {
const isActiveTab = activeTab === index;
return (
<View
style={{
width: tabWidth,
borderBottomWidth: isActiveTab ? 2 : 1,
borderColor: isActiveTab ? colors.tintActive : colors.separatorColor
}}
>
<TabBarItem tab={tab} index={index} goToPage={goToPage} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} />
</View>
);
})}
</ScrollView>
</View>
);
};
const UsersList = ({ tabLabel }: { tabLabel: IReaction }) => {
const { colors } = useTheme();
const { emoji, usernames } = tabLabel;
return (
<FlatList
data={usernames}
ListHeaderComponent={() => (
<View style={styles.emojiName}>
<Text style={{ color: colors.auxiliaryTintColor }}>{emoji}</Text>
</View>
)}
renderItem={({ item }) => (
<View style={styles.userItemContainer}>
<Avatar text={item} size={36} />
<View style={styles.usernameContainer}>
<Text style={[styles.usernameText, { color: colors.titleText }]}>{item}</Text>
</View>
</View>
)}
keyExtractor={item => item}
/>
);
};
const ReactionsList = ({ reactions, baseUrl, getCustomEmoji, width }: IReactionsListProps): React.ReactElement => {
// sorting reactions in descending order on the basic of number of users reacted
const sortedReactions = reactions?.sort((reaction1, reaction2) => reaction2.usernames.length - reaction1.usernames.length);
return (
<View style={styles.reactionsListContainer}>
<ScrollableTabView renderTabBar={() => <ReactionsTabBar baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} width={width} />}>
{sortedReactions?.map(reaction => (
<UsersList tabLabel={reaction} key={reaction.emoji} />
))}
</ScrollableTabView>
</View>
);
};
export default ReactionsList;

View File

@ -0,0 +1,75 @@
import React from 'react';
import { Text, View, FlatList } from 'react-native';
import Emoji from '../message/Emoji';
import { useTheme } from '../../theme';
import { IReaction } from '../../definitions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n';
import styles from './styles';
import { useAppSelector } from '../../lib/hooks';
interface IAllReactionsListItemProps {
getCustomEmoji: TGetCustomEmoji;
item: IReaction;
}
interface IAllTabProps {
getCustomEmoji: TGetCustomEmoji;
tabLabel: IReaction;
reactions?: IReaction[];
}
const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => {
const { colors } = useTheme();
const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name);
const username = useAppSelector(state => state.login.user.username);
const count = item.usernames.length;
let displayNames;
if (useRealName && item.names) {
displayNames = item.names
.slice(0, 3)
.map((name, index) => (item.usernames[index] === username ? I18n.t('you') : name))
.join(', ');
} else {
displayNames = item.usernames
.slice(0, 3)
.map((otherUsername: string) => (username === otherUsername ? I18n.t('you') : otherUsername))
.join(', ');
}
if (count > 3) {
displayNames = `${displayNames} ${I18n.t('and_more')} ${count - 3}`;
} else {
displayNames = displayNames.replace(/,(?=[^,]*$)/, ` ${I18n.t('and')}`);
}
return (
<View style={styles.listItemContainer}>
<Emoji
content={item.emoji}
standardEmojiStyle={styles.allTabStandardEmojiStyle}
customEmojiStyle={styles.allTabCustomEmojiStyle}
getCustomEmoji={getCustomEmoji}
/>
<View style={styles.textContainer}>
<Text style={[styles.allListNPeopleReacted, { color: colors.bodyText }]}>
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
</Text>
<Text style={[styles.allListWhoReacted, { color: colors.auxiliaryText }]}>{displayNames}</Text>
</View>
</View>
);
};
const AllTab = ({ reactions, getCustomEmoji }: IAllTabProps): React.ReactElement => (
<View style={styles.allTabContainer} testID='reactionsListAllTab'>
<FlatList
data={reactions}
contentContainerStyle={styles.listContainer}
renderItem={({ item }) => <AllReactionsListItem item={item} getCustomEmoji={getCustomEmoji} />}
keyExtractor={item => item.emoji}
/>
</View>
);
export default AllTab;

View File

@ -0,0 +1,71 @@
import React from 'react';
import { View } from 'react-native';
import { TGetCustomEmoji, ICustomEmoji } from '../../definitions';
import ReactionsList from '.';
import { mockedStore as store } from '../../reducers/mockedStore';
import { updateSettings } from '../../actions/settings';
const getCustomEmoji: TGetCustomEmoji = content => {
const customEmoji = {
marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' }
}[content] as ICustomEmoji;
return customEmoji;
};
const reactions = [
{
emoji: ':marioparty:',
_id: 'marioparty',
usernames: ['rocket.cat', 'diego.mello'],
names: ['Rocket Cat', 'Diego Mello']
},
{
emoji: ':react_rocket:',
_id: 'react_rocket',
usernames: ['rocket.cat', 'diego.mello'],
names: ['Rocket Cat', 'Diego Mello']
},
{
emoji: ':nyan_rocket:',
_id: 'nyan_rocket',
usernames: ['rocket.cat'],
names: ['Rocket Cat']
},
{
emoji: ':grinning:',
_id: 'grinning',
usernames: ['diego.mello'],
names: ['Diego Mello']
},
{
emoji: ':tada:',
_id: 'tada',
usernames: ['diego.mello'],
names: ['Diego Mello']
}
];
export const ReactionsListStory = () => {
store.dispatch(updateSettings('UI_Use_Real_Name', false));
return (
<View style={{ paddingVertical: 10, flex: 1 }}>
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
</View>
);
};
export const ReactionsListFullName = () => {
store.dispatch(updateSettings('UI_Use_Real_Name', true));
return (
<View style={{ paddingVertical: 10, flex: 1 }}>
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
</View>
);
};
export default {
title: 'ReactionsList'
};

View File

@ -0,0 +1,79 @@
import React from 'react';
import { fireEvent, render, within } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import ReactionsList from '.';
import { mockedStore } from '../../reducers/mockedStore';
const getCustomEmoji = jest.fn();
const reactions = [
{
emoji: 'marioparty',
_id: 'marioparty',
usernames: ['rocket.cat', 'diego.mello'],
names: ['Rocket Cat', 'Diego Mello']
},
{
emoji: 'react_rocket',
_id: 'react_rocket',
usernames: ['rocket.cat', 'diego.mello'],
names: ['Rocket Cat', 'Diego Mello']
},
{
emoji: 'nyan_rocket',
_id: 'nyan_rocket',
usernames: ['rocket.cat'],
names: ['Rocket Cat']
},
{
emoji: 'grinning',
_id: 'grinning',
usernames: ['diego.mello'],
names: ['Diego Mello']
}
];
const Render = () => (
<Provider store={mockedStore}>
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
</Provider>
);
describe('ReactionsList', () => {
test('should render Reactions List', async () => {
const { findByTestId } = render(<Render />);
const ReactionsListView = await findByTestId('reactionsList');
expect(ReactionsListView).toBeTruthy();
});
test('should render tab bar', async () => {
const { findByTestId } = render(<Render />);
const AllTab = await findByTestId('reactionsTabBar');
expect(AllTab).toBeTruthy();
});
test('should render All tab', async () => {
const { findByTestId } = render(<Render />);
const AllTab = await findByTestId('reactionsListAllTab');
expect(AllTab).toBeTruthy();
});
test('correct tab on clicking tab item', async () => {
const { findByTestId } = render(<Render />);
const tab = await findByTestId(`tabBarItem-${reactions[0].emoji}`);
fireEvent.press(tab);
const usersList = await findByTestId(`usersList-${reactions[0].emoji}`);
expect(usersList).toBeTruthy();
const emojiName = await within(usersList).getByTestId(`usersListEmojiName`);
expect(emojiName.props.children).toEqual(reactions[0].emoji);
});
test('should render correct number of reactions', async () => {
const { findByTestId } = render(<Render />);
const tab = await findByTestId(`tabBarItem-${reactions[0].emoji}`);
fireEvent.press(tab);
const usersList = await findByTestId(`usersList-${reactions[0].emoji}`);
const allReactions = await within(usersList).getAllByTestId('userItem');
expect(allReactions).toHaveLength(reactions[0].usernames.length);
});
});

View File

@ -0,0 +1,86 @@
import React from 'react';
import { Text, Pressable, View, ScrollView } from 'react-native';
import Emoji from '../message/Emoji';
import { useTheme } from '../../theme';
import { IReaction } from '../../definitions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n';
import styles, { MIN_TAB_WIDTH } from './styles';
import { useDimensions, useOrientation } from '../../dimensions';
interface ITabBarItem {
getCustomEmoji: TGetCustomEmoji;
tab: IReaction;
index: number;
goToPage?: (index: number) => void;
}
interface IReactionsTabBar {
getCustomEmoji: TGetCustomEmoji;
activeTab?: number;
tabs?: IReaction[];
goToPage?: (index: number) => void;
}
const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme();
return (
<Pressable
key={tab.emoji}
onPress={() => {
goToPage?.(index);
}}
style={({ pressed }: { pressed: boolean }) => ({
opacity: pressed ? 0.7 : 1
})}
testID={`tabBarItem-${tab.emoji}`}
>
<View style={styles.tabBarItem}>
{tab._id === 'All' ? (
<Text style={[styles.allTabItem, { color: colors.auxiliaryTintColor }]}>{I18n.t('All')}</Text>
) : (
<>
<Emoji
content={tab.emoji}
standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
</>
)}
</View>
</Pressable>
);
};
const ReactionsTabBar = ({ tabs, activeTab, goToPage, getCustomEmoji }: IReactionsTabBar): React.ReactElement => {
const { isLandscape } = useOrientation();
const { width } = useDimensions();
const reactionsListWidth = isLandscape ? width / 2 : width;
const tabWidth = tabs && Math.max(reactionsListWidth / tabs.length, MIN_TAB_WIDTH);
const { colors } = useTheme();
return (
<View testID='reactionsTabBar'>
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
{tabs?.map((tab, index) => {
const isActiveTab = activeTab === index;
return (
<View
style={{
width: tabWidth,
borderBottomWidth: isActiveTab ? 2 : 1,
borderColor: isActiveTab ? colors.tintActive : colors.separatorColor
}}
key={tab.emoji}
>
<TabBarItem tab={tab} index={index} goToPage={goToPage} getCustomEmoji={getCustomEmoji} />
</View>
);
})}
</ScrollView>
</View>
);
};
export default ReactionsTabBar;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Text, View, FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { useTheme } from '../../theme';
import { IReaction, IApplicationState } from '../../definitions';
import Avatar from '../Avatar';
import styles from './styles';
const UsersList = ({ tabLabel }: { tabLabel: IReaction }): React.ReactElement => {
const { colors } = useTheme();
const useRealName = useSelector((state: IApplicationState) => state.settings.UI_Use_Real_Name);
const { emoji, usernames, names } = tabLabel;
const users =
names?.length > 0
? usernames.map((username, index) => ({ username, name: names[index] }))
: usernames.map(username => ({ username, name: '' }));
return (
<FlatList
data={users}
contentContainerStyle={styles.listContainer}
ListHeaderComponent={
<View style={styles.emojiNameContainer}>
<Text style={[styles.emojiName, { color: colors.auxiliaryText }]} testID='usersListEmojiName'>
{emoji}
</Text>
</View>
}
renderItem={({ item }) => (
<View style={styles.listItemContainer} testID='userItem'>
<Avatar text={item.username} size={36} />
<View style={styles.textContainer}>
<Text style={[styles.usernameText, { color: colors.bodyText }]} numberOfLines={1}>
{useRealName && item.name ? item.name : item.username}
</Text>
</View>
</View>
)}
keyExtractor={item => item.username}
testID={`usersList-${emoji}`}
/>
);
};
export default UsersList;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IReaction } from '../../definitions';
import I18n from '../../i18n';
import styles from './styles';
import AllTab from './AllTab';
import UsersList from './UsersList';
import ReactionsTabBar from './ReactionsTabBar';
interface IReactionsListProps {
getCustomEmoji: TGetCustomEmoji;
reactions?: IReaction[];
}
const ReactionsList = ({ reactions, getCustomEmoji }: IReactionsListProps): React.ReactElement => {
// sorting reactions in descending order on the basic of number of users reacted
const sortedReactions = reactions?.sort((reaction1, reaction2) => reaction2.usernames.length - reaction1.usernames.length);
const allTabLabel = { emoji: I18n.t('All'), usernames: [], names: [], _id: 'All' };
return (
<View style={styles.container} testID='reactionsList'>
<ScrollableTabView renderTabBar={() => <ReactionsTabBar getCustomEmoji={getCustomEmoji} />}>
<AllTab tabLabel={allTabLabel} reactions={sortedReactions} getCustomEmoji={getCustomEmoji} />
{sortedReactions?.map(reaction => (
<UsersList tabLabel={reaction} key={reaction.emoji} />
))}
</ScrollableTabView>
</View>
);
};
export default ReactionsList;

View File

@ -0,0 +1,88 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export const MIN_TAB_WIDTH = 70;
export default StyleSheet.create({
container: {
flex: 1
},
allTabContainer: {
flex: 1
},
tabBarItem: {
paddingBottom: 4,
height: 44,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
listContainer: {
marginHorizontal: 12,
marginVertical: 8,
paddingBottom: 30
},
reactionCount: {
marginLeft: 4,
...sharedStyles.textSemibold
},
emojiNameContainer: {
marginVertical: 8
},
emojiName: {
fontSize: 14,
...sharedStyles.textMedium
},
listItemContainer: {
marginVertical: 6,
flexDirection: 'row',
alignItems: 'center'
},
textContainer: {
flex: 1,
marginLeft: 8,
justifyContent: 'center'
},
usernameText: {
fontSize: 16,
...sharedStyles.textSemibold
},
standardEmojiStyle: {
fontSize: 20,
width: 24,
height: 24,
textAlign: 'center',
color: '#fff'
},
customEmojiStyle: {
width: 24,
height: 24
},
allTabItem: {
fontSize: 16,
...sharedStyles.textSemibold
},
allTabStandardEmojiStyle: {
fontSize: 30,
width: 36,
textAlign: 'center',
color: '#fff'
},
allTabCustomEmojiStyle: {
width: 36,
height: 36
},
allListItemContainer: {
flexDirection: 'row',
alignItems: 'center'
},
allListNPeopleReacted: {
fontSize: 14,
...sharedStyles.textMedium
},
allListWhoReacted: {
fontSize: 14,
...sharedStyles.textRegular
}
});

View File

@ -7,6 +7,7 @@ import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon'; import RoomTypeIcon from '../RoomTypeIcon';
import { TUserStatus, IOmnichannelSource } from '../../definitions'; import { TUserStatus, IOmnichannelSource } from '../../definitions';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { useAppSelector } from '../../lib/hooks';
const HIT_SLOP = { const HIT_SLOP = {
top: 5, top: 5,
@ -141,8 +142,9 @@ const Header = React.memo(
const { colors } = useTheme(); const { colors } = useTheme();
const portrait = height > width; const portrait = height > width;
let scale = 1; let scale = 1;
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
if (!portrait && !tmid) { if (!portrait && !tmid && !isMasterDetail) {
if (usersTyping.length > 0 || subtitle) { if (usersTyping.length > 0 || subtitle) {
scale = 0.8; scale = 0.8;
} }

View File

@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { StyleSheet, TextInputProps, View } from 'react-native'; import { StyleSheet, TextInputProps, View } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { FormTextInput } from '../TextInput'; import { FormTextInput } from '../TextInput';
@ -14,13 +15,15 @@ const styles = StyleSheet.create({
const SearchBox = ({ onChangeText, onSubmitEditing, testID }: TextInputProps): JSX.Element => { const SearchBox = ({ onChangeText, onSubmitEditing, testID }: TextInputProps): JSX.Element => {
const [text, setText] = useState(''); const [text, setText] = useState('');
const { colors } = useTheme();
const internalOnChangeText = useCallback(value => { const internalOnChangeText = useCallback(value => {
setText(value); setText(value);
onChangeText?.(value); onChangeText?.(value);
}, []); }, []);
return ( return (
<View testID='searchbox'> <View testID='searchbox' style={{ backgroundColor: colors.backgroundColor }}>
<FormTextInput <FormTextInput
autoCapitalize='none' autoCapitalize='none'
autoCorrect={false} autoCorrect={false}

View File

@ -26,9 +26,10 @@ const styles = StyleSheet.create({
...sharedStyles.textRegular, ...sharedStyles.textRegular,
height: 48, height: 48,
fontSize: 16, fontSize: 16,
padding: 14, paddingHorizontal: 16,
borderWidth: StyleSheet.hairlineWidth, paddingVertical: 10,
borderRadius: 2 borderWidth: 1,
borderRadius: 4
}, },
inputIconLeft: { inputIconLeft: {
paddingLeft: 45 paddingLeft: 45
@ -37,17 +38,17 @@ const styles = StyleSheet.create({
paddingRight: 45 paddingRight: 45
}, },
wrap: { wrap: {
position: 'relative' position: 'relative',
justifyContent: 'center'
}, },
iconContainer: { iconContainer: {
position: 'absolute', position: 'absolute'
top: 14
}, },
iconLeft: { iconLeft: {
left: 15 left: 12
}, },
iconRight: { iconRight: {
right: 15 right: 12
} }
}); });
@ -98,7 +99,7 @@ export const FormTextInput = ({
style={[ style={[
styles.input, styles.input,
iconLeft && styles.inputIconLeft, iconLeft && styles.inputIconLeft,
(secureTextEntry || iconRight) && styles.inputIconRight, (secureTextEntry || iconRight || showClearInput) && styles.inputIconRight,
{ {
backgroundColor: colors.backgroundColor, backgroundColor: colors.backgroundColor,
borderColor: colors.separatorColor, borderColor: colors.separatorColor,

View File

@ -27,3 +27,44 @@ export const ShortAndLong = () => (
</View> </View>
</> </>
); );
export const Icons = () => (
<>
<View style={styles.paddingHorizontal}>
<FormTextInput label='Right icon' placeholder='placeholder' value={item.name} iconRight={'close'} />
<FormTextInput label='Left icon' placeholder='placeholder' value={item.longText} iconLeft={'mail'} />
<FormTextInput label='Both icons' placeholder='placeholder' value={item.longText} iconLeft={'mail'} iconRight={'add'} />
<FormTextInput
label='Icon and touchable clear input'
placeholder='placeholder'
value={item.longText}
onClearInput={() => {}}
/>
</View>
</>
);
export const Multiline = () => (
<>
<View style={styles.paddingHorizontal}>
<FormTextInput label='Multiline text' placeholder='placeholder' multiline value={`${item.name}\n\n${item.longText}\n`} />
</View>
</>
);
export const SecureTextEntry = () => (
<>
<View style={styles.paddingHorizontal}>
<FormTextInput label='Secure text disabled' placeholder='placeholder' value={item.name} />
<FormTextInput label='Secure text enabled' placeholder='placeholder' value={item.name} secureTextEntry />
</View>
</>
);
export const Loading = () => (
<>
<View style={styles.paddingHorizontal}>
<FormTextInput label='Loading false' placeholder='placeholder' value={item.name} loading={false} />
<FormTextInput label='Loading true' placeholder='placeholder' value={item.name} loading />
</View>
</>
);

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, unstable_batchedUpdates, View } from 'react-native';
import DateTimePicker, { Event } from '@react-native-community/datetimepicker'; import DateTimePicker, { Event } from '@react-native-community/datetimepicker';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { BlockContext } from '@rocket.chat/ui-kit'; import { BlockContext } from '@rocket.chat/ui-kit';
@ -19,8 +19,8 @@ const styles = StyleSheet.create({
input: { input: {
height: 48, height: 48,
paddingLeft: 16, paddingLeft: 16,
borderWidth: StyleSheet.hairlineWidth, borderWidth: 1,
borderRadius: 2, borderRadius: 4,
alignItems: 'center', alignItems: 'center',
flexDirection: 'row' flexDirection: 'row'
}, },
@ -48,11 +48,15 @@ export const DatePicker = ({ element, language, action, context, loading, value,
// timestamp as number exists in Event // timestamp as number exists in Event
// @ts-ignore // @ts-ignore
const onChange = ({ nativeEvent: { timestamp } }: Event, date?: Date) => { const onChange = ({ nativeEvent: { timestamp } }: Event, date?: Date) => {
const newDate = date || new Date(timestamp); if (date || timestamp) {
onChangeDate(newDate); const newDate = date || new Date(timestamp);
action({ value: moment(newDate).format('YYYY-MM-DD') }); unstable_batchedUpdates(() => {
if (isAndroid) { onChangeDate(newDate);
onShow(false); if (isAndroid) {
onShow(false);
}
});
action({ value: moment(newDate).format('YYYY-MM-DD') });
} }
}; };
@ -85,7 +89,13 @@ export const DatePicker = ({ element, language, action, context, loading, value,
} }
const content = show ? ( const content = show ? (
<DateTimePicker mode='date' display='default' value={currentDate} onChange={onChange} textColor={themes[theme].titleText} /> <DateTimePicker
mode='date'
display={isAndroid ? 'default' : 'inline'}
value={currentDate}
onChange={onChange}
textColor={themes[theme].titleText}
/>
) : null; ) : null;
return ( return (

View File

@ -22,11 +22,11 @@ const Input = ({ children, onPress, loading, inputStyle, placeholder, disabled,
return ( return (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
style={[{ backgroundColor: colors.backgroundColor }, inputStyle]} style={[{ backgroundColor: colors.backgroundColor }, styles.inputBorder, inputStyle]}
background={Touchable.Ripple(colors.bannerBackground)} background={Touchable.Ripple(colors.bannerBackground)}
disabled={disabled} disabled={disabled}
> >
<View style={[styles.input, { borderColor: colors.separatorColor }, innerInputStyle]}> <View style={[styles.input, styles.inputBorder, { borderColor: colors.separatorColor }, innerInputStyle]}>
{placeholder ? <Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder}</Text> : children} {placeholder ? <Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder}</Text> : children}
{loading ? ( {loading ? (
<ActivityIndicator style={styles.icon} /> <ActivityIndicator style={styles.icon} />

View File

@ -31,12 +31,14 @@ export default StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
flex: 1 flex: 1
}, },
inputBorder: {
borderRadius: 4
},
input: { input: {
minHeight: 48, minHeight: 48,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingBottom: 0, paddingBottom: 0,
borderWidth: StyleSheet.hairlineWidth, borderWidth: 1,
borderRadius: 2,
alignItems: 'center', alignItems: 'center',
flexDirection: 'row' flexDirection: 'row'
}, },

View File

@ -19,8 +19,8 @@ const styles = StyleSheet.create({
viewContainer: { viewContainer: {
marginBottom: 16, marginBottom: 16,
paddingHorizontal: 16, paddingHorizontal: 16,
borderWidth: StyleSheet.hairlineWidth, borderWidth: 1,
borderRadius: 2, borderRadius: 4,
justifyContent: 'center' justifyContent: 'center'
}, },
pickerText: { pickerText: {

View File

@ -10,33 +10,24 @@ interface IEmoji {
literal: string; literal: string;
isMessageContainsOnlyEmoji: boolean; isMessageContainsOnlyEmoji: boolean;
getCustomEmoji?: Function; getCustomEmoji?: Function;
baseUrl: string;
customEmojis?: any; customEmojis?: any;
style?: object; style?: object;
onEmojiSelected?: Function; onEmojiSelected?: Function;
tabEmojiStyle?: object; tabEmojiStyle?: object;
} }
const Emoji = React.memo( const Emoji = React.memo(({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, customEmojis = true, style = {} }: IEmoji) => {
({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = {} }: IEmoji) => { const { colors } = useTheme();
const { colors } = useTheme(); const emojiUnicode = shortnameToUnicode(literal);
const emojiUnicode = shortnameToUnicode(literal); const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, '')); if (emoji && customEmojis) {
if (emoji && customEmojis) { return <CustomEmoji style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]} emoji={emoji} />;
return (
<CustomEmoji
baseUrl={baseUrl}
style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]}
emoji={emoji}
/>
);
}
return (
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode}
</Text>
);
} }
); return (
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode}
</Text>
);
});
export default Emoji; export default Emoji;

View File

@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
import Markdown, { MarkdownPreview } from '.'; import Markdown, { MarkdownPreview } from '.';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { TGetCustomEmoji, IEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji, ICustomEmoji } from '../../definitions/IEmoji';
const theme = 'light'; const theme = 'light';
@ -16,7 +16,6 @@ const styles = StyleSheet.create({
} }
}); });
const baseUrl = 'https://open.rocket.chat';
const longText = const longText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const lineBreakText = `a const lineBreakText = `a
@ -34,7 +33,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
marioparty: { name: content, extension: 'gif' }, marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' }, react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' } nyan_rocket: { name: content, extension: 'png' }
}[content] as IEmoji; }[content] as ICustomEmoji;
return customEmoji; return customEmoji;
}; };
@ -108,13 +107,8 @@ export const Emoji = () => (
<View style={styles.container}> <View style={styles.container}>
<Markdown msg='Unicode: 😃😇👍' theme={theme} /> <Markdown msg='Unicode: 😃😇👍' theme={theme} />
<Markdown msg='Shortnames: :joy::+1:' theme={theme} /> <Markdown msg='Shortnames: :joy::+1:' theme={theme} />
<Markdown <Markdown msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:' <Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
theme={theme}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
/>
<Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
</View> </View>
); );

View File

@ -33,7 +33,6 @@ interface IMarkdownProps {
md?: MarkdownAST; md?: MarkdownAST;
mentions?: IUserMention[]; mentions?: IUserMention[];
getCustomEmoji?: TGetCustomEmoji; getCustomEmoji?: TGetCustomEmoji;
baseUrl?: string;
username?: string; username?: string;
tmid?: string; tmid?: string;
numberOfLines?: number; numberOfLines?: number;
@ -46,6 +45,7 @@ interface IMarkdownProps {
testID?: string; testID?: string;
style?: StyleProp<TextStyle>[]; style?: StyleProp<TextStyle>[];
onLinkPress?: TOnLinkPress; onLinkPress?: TOnLinkPress;
isTranslated?: boolean;
} }
type TLiteral = { type TLiteral = {
@ -94,9 +94,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
constructor(props: IMarkdownProps) { constructor(props: IMarkdownProps) {
super(props); super(props);
if (!this.isNewMarkdown) { this.renderer = this.createRenderer();
this.renderer = this.createRenderer();
}
} }
createRenderer = () => createRenderer = () =>
@ -235,13 +233,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderEmoji = ({ literal }: TLiteral) => { renderEmoji = ({ literal }: TLiteral) => {
const { getCustomEmoji, baseUrl = '', customEmojis, style } = this.props; const { getCustomEmoji, customEmojis, style } = this.props;
return ( return (
<MarkdownEmoji <MarkdownEmoji
literal={literal} literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji} isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
customEmojis={customEmojis} customEmojis={customEmojis}
style={style} style={style}
/> />
@ -321,19 +318,18 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
useRealName, useRealName,
username = '', username = '',
getCustomEmoji, getCustomEmoji,
baseUrl = '', onLinkPress,
onLinkPress isTranslated
} = this.props; } = this.props;
if (!msg) { if (!msg) {
return null; return null;
} }
if (this.isNewMarkdown) { if (this.isNewMarkdown && !isTranslated) {
return ( return (
<NewMarkdown <NewMarkdown
username={username} username={username}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
useRealName={useRealName} useRealName={useRealName}
tokens={md} tokens={md}
@ -350,7 +346,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
let ast = parser.parse(m); let ast = parser.parse(m);
ast = mergeTextNodes(ast); ast = mergeTextNodes(ast);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3; this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
return this.renderer.render(ast); return this.renderer?.render(ast) || null;
} }
} }

View File

@ -3,7 +3,6 @@ import { Text } from 'react-native';
import { Emoji as EmojiProps } from '@rocket.chat/message-parser'; import { Emoji as EmojiProps } from '@rocket.chat/message-parser';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme'; import { useTheme } from '../../../theme';
import styles from '../styles'; import styles from '../styles';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
@ -15,21 +14,21 @@ interface IEmojiProps {
} }
const Emoji = ({ block, isBigEmoji }: IEmojiProps) => { const Emoji = ({ block, isBigEmoji }: IEmojiProps) => {
const { theme } = useTheme(); const { colors } = useTheme();
const { baseUrl, getCustomEmoji } = useContext(MarkdownContext); const { getCustomEmoji } = useContext(MarkdownContext);
if ('unicode' in block) { if ('unicode' in block) {
return <Text style={[{ color: themes[theme].bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>; return <Text style={[{ color: colors.bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>;
} }
const emojiToken = block?.shortCode ? `:${block.shortCode}:` : `:${block.value?.value}:`; const emojiToken = block?.shortCode ? `:${block.shortCode}:` : `:${block.value?.value}:`;
const emojiUnicode = shortnameToUnicode(emojiToken); const emojiUnicode = shortnameToUnicode(emojiToken);
const emoji = getCustomEmoji?.(block.value?.value); const emoji = getCustomEmoji?.(block.value?.value);
if (emoji) { if (emoji) {
return <CustomEmoji baseUrl={baseUrl} style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />; return <CustomEmoji style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />;
} }
return ( return (
<Text style={[{ color: themes[theme].bodyText }, isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text]}> <Text style={[{ color: colors.bodyText }, isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text]}>
{emojiUnicode} {emojiUnicode}
</Text> </Text>
); );

View File

@ -41,18 +41,21 @@ const Link = ({ value }: ILinkProps) => {
return ( return (
<Text onPress={handlePress} onLongPress={onLongPress} style={[styles.link, { color: themes[theme].actionTintColor }]}> <Text onPress={handlePress} onLongPress={onLongPress} style={[styles.link, { color: themes[theme].actionTintColor }]}>
{(block => { {(block => {
switch (block.type) { const blockArray = Array.isArray(block) ? block : [block];
case 'PLAIN_TEXT': return blockArray.map(blockInArray => {
return block.value; switch (blockInArray.type) {
case 'STRIKE': case 'PLAIN_TEXT':
return <Strike value={block.value} />; return blockInArray.value;
case 'ITALIC': case 'STRIKE':
return <Italic value={block.value} />; return <Strike value={blockInArray.value} />;
case 'BOLD': case 'ITALIC':
return <Bold value={block.value} />; return <Italic value={blockInArray.value} />;
default: case 'BOLD':
return null; return <Bold value={blockInArray.value} />;
} default:
return null;
}
});
})(label)} })(label)}
</Text> </Text>
); );

View File

@ -7,7 +7,6 @@ interface IMarkdownContext {
channels?: IUserChannel[]; channels?: IUserChannel[];
useRealName?: boolean; useRealName?: boolean;
username?: string; username?: string;
baseUrl?: string;
navToRoomInfo?: Function; navToRoomInfo?: Function;
getCustomEmoji?: Function; getCustomEmoji?: Function;
onLinkPress?: Function; onLinkPress?: Function;
@ -18,7 +17,6 @@ const defaultState = {
channels: [], channels: [],
useRealName: false, useRealName: false,
username: '', username: '',
baseUrl: '',
navToRoomInfo: () => {} navToRoomInfo: () => {}
}; };

View File

@ -3,22 +3,25 @@ import { StyleSheet, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import NewMarkdownComponent from '.'; import NewMarkdownComponent from '.';
import { themes } from '../../../lib/constants'; import { colors, themes } from '../../../lib/constants';
import { longText } from '../../../../.storybook/utils'; import { longText } from '../../../../.storybook/utils';
import { ThemeContext } from '../../../theme';
const theme = 'light';
export default { export default {
title: 'NewMarkdown', title: 'NewMarkdown',
decorators: [ decorators: [
(Story: any) => ( (Story: any) => (
<NavigationContainer> <NavigationContainer>
<Story /> <ThemeContext.Provider value={{ theme, colors: colors[theme] }}>
<Story />
</ThemeContext.Provider>
</NavigationContainer> </NavigationContainer>
) )
] ]
}; };
const theme = 'light';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
marginHorizontal: 15, marginHorizontal: 15,
@ -34,11 +37,8 @@ const getCustomEmoji = (content: string) => {
}[content]; }[content];
return customEmoji; return customEmoji;
}; };
const baseUrl = 'https://open.rocket.chat';
const NewMarkdown = ({ ...props }) => ( const NewMarkdown = ({ ...props }) => <NewMarkdownComponent getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />;
<NewMarkdownComponent baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />
);
const simpleTextMsg = [ const simpleTextMsg = [
{ {
@ -340,7 +340,7 @@ const emojiTokens = [
export const Emoji = () => ( export const Emoji = () => (
<View style={styles.container}> <View style={styles.container}>
<NewMarkdown tokens={bigEmojiTokens} /> <NewMarkdown tokens={bigEmojiTokens} />
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} /> <NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} />
</View> </View>
); );
@ -423,10 +423,73 @@ const markdownLink = [
} }
]; ];
const markdownLinkWithEmphasis = [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat/'
},
label: [
{
type: 'PLAIN_TEXT',
value: 'Normal Link - '
},
{
type: 'BOLD',
value: [
{
type: 'PLAIN_TEXT',
value: 'Bold'
}
]
},
{
type: 'PLAIN_TEXT',
value: ' '
},
{
type: 'STRIKE',
value: [
{
type: 'PLAIN_TEXT',
value: 'strike'
}
]
},
{
type: 'PLAIN_TEXT',
value: ' and '
},
{
type: 'ITALIC',
value: [
{
type: 'PLAIN_TEXT',
value: 'Italic'
}
]
},
{
type: 'PLAIN_TEXT',
value: ' Styles'
}
]
}
}
]
}
];
export const Links = () => ( export const Links = () => (
<View style={styles.container}> <View style={styles.container}>
<NewMarkdown tokens={rocketChatLink} /> <NewMarkdown tokens={rocketChatLink} />
<NewMarkdown tokens={markdownLink} /> <NewMarkdown tokens={markdownLink} />
<NewMarkdown tokens={markdownLinkWithEmphasis} />
</View> </View>
); );

View File

@ -3,20 +3,15 @@ import { Text } from 'react-native';
import { Plain as PlainProps } from '@rocket.chat/message-parser'; import { Plain as PlainProps } from '@rocket.chat/message-parser';
import styles from '../styles'; import styles from '../styles';
import { useTheme } from '../../../theme';
import { themes } from '../../../lib/constants';
interface IPlainProps { interface IPlainProps {
value: PlainProps['value']; value: PlainProps['value'];
} }
const Plain = ({ value }: IPlainProps) => { const Plain = ({ value }: IPlainProps) => (
const { theme } = useTheme(); <Text accessibilityLabel={value} style={styles.plainText}>
return ( {value}
<Text accessibilityLabel={value} style={[styles.plainText, { color: themes[theme].bodyText }]}> </Text>
{value} );
</Text>
);
};
export default Plain; export default Plain;

View File

@ -24,7 +24,6 @@ interface IBodyProps {
navToRoomInfo?: Function; navToRoomInfo?: Function;
useRealName?: boolean; useRealName?: boolean;
username: string; username: string;
baseUrl: string;
} }
const Body = ({ const Body = ({
@ -35,7 +34,6 @@ const Body = ({
username, username,
navToRoomInfo, navToRoomInfo,
getCustomEmoji, getCustomEmoji,
baseUrl,
onLinkPress onLinkPress
}: IBodyProps): React.ReactElement | null => { }: IBodyProps): React.ReactElement | null => {
if (isEmpty(tokens)) { if (isEmpty(tokens)) {
@ -51,7 +49,6 @@ const Body = ({
username, username,
navToRoomInfo, navToRoomInfo,
getCustomEmoji, getCustomEmoji,
baseUrl,
onLinkPress onLinkPress
}} }}
> >

View File

@ -57,7 +57,7 @@ const AttachedActions = ({ attachment }: { attachment: IAttachment }) => {
}; };
const Attachments: React.FC<IMessageAttachments> = React.memo( const Attachments: React.FC<IMessageAttachments> = React.memo(
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => { ({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, id }: IMessageAttachments) => {
const { theme } = useTheme(); const { theme } = useTheme();
if (!attachments || attachments.length === 0) { if (!attachments || attachments.length === 0) {
@ -80,7 +80,15 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
if (file && file.audio_url) { if (file && file.audio_url) {
return ( return (
<Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} isReply={isReply} style={style} theme={theme} /> <Audio
key={file.audio_url}
file={file}
getCustomEmoji={getCustomEmoji}
isReply={isReply}
style={style}
theme={theme}
messageId={id}
/>
); );
} }
@ -106,7 +114,16 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
); );
} }
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />; return (
<Reply
key={index}
index={index}
attachment={file}
timeFormat={timeFormat}
getCustomEmoji={getCustomEmoji}
messageId={id}
/>
);
}); });
return <>{attachmentsElements}</>; return <>{attachmentsElements}</>;
}, },

View File

@ -20,6 +20,8 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment } from '../../definitions'; import { IAttachment } from '../../definitions';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import { downloadAudioFile } from '../../lib/methods/audioFile'; import { downloadAudioFile } from '../../lib/methods/audioFile';
import EventEmitter from '../../lib/methods/helpers/events';
import { PAUSE_AUDIO } from './constants';
interface IButton { interface IButton {
loading: boolean; loading: boolean;
@ -36,6 +38,7 @@ interface IMessageAudioProps {
theme: TSupportedThemes; theme: TSupportedThemes;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
scale?: number; scale?: number;
messageId: string;
} }
interface IMessageAudioState { interface IMessageAudioState {
@ -127,8 +130,13 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate); this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
} }
pauseSound = () => {
EventEmitter.removeListener(PAUSE_AUDIO, this.pauseSound);
this.togglePlayPause();
};
async componentDidMount() { async componentDidMount() {
const { file } = this.props; const { file, messageId } = this.props;
const { baseUrl, user } = this.context; const { baseUrl, user } = this.context;
let url = file.audio_url; let url = file.audio_url;
@ -139,7 +147,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
if (url) { if (url) {
const audio = await downloadAudioFile(`${url}?rc_uid=${user.id}&rc_token=${user.token}`, url); const audio = await downloadAudioFile(`${url}?rc_uid=${user.id}&rc_token=${user.token}`, url, messageId);
await this.sound.loadAsync({ uri: audio }); await this.sound.loadAsync({ uri: audio });
} }
} catch { } catch {
@ -182,6 +190,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
} }
async componentWillUnmount() { async componentWillUnmount() {
EventEmitter.removeListener(PAUSE_AUDIO, this.pauseSound);
try { try {
await this.sound.stopAsync(); await this.sound.stopAsync();
} catch { } catch {
@ -220,6 +229,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
try { try {
await this.sound.stopAsync(); await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 }); this.setState({ paused: true, currentTime: 0 });
EventEmitter.removeListener(PAUSE_AUDIO, this.pauseSound);
} catch { } catch {
// do nothing // do nothing
} }
@ -242,7 +252,10 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
try { try {
if (paused) { if (paused) {
await this.sound.pauseAsync(); await this.sound.pauseAsync();
EventEmitter.removeListener(PAUSE_AUDIO, this.pauseSound);
} else { } else {
EventEmitter.emit(PAUSE_AUDIO);
EventEmitter.addEventListener(PAUSE_AUDIO, this.pauseSound);
await Audio.setAudioModeAsync(mode); await Audio.setAudioModeAsync(mode);
await this.sound.playAsync(); await this.sound.playAsync();
} }
@ -282,7 +295,6 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
<Markdown <Markdown
msg={description} msg={description}
style={[isReply && style]} style={[isReply && style]}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}

View File

@ -30,7 +30,7 @@ export const Item = () => (
user: { username: 'Marcos' } user: { username: 'Marcos' }
}} }}
> >
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => {}} timeFormat='LT' /> <CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' />
</MessageContext.Provider> </MessageContext.Provider>
</View> </View>
); );

View File

@ -82,7 +82,7 @@ interface IMessageReply {
const Fields = React.memo( const Fields = React.memo(
({ attachment, getCustomEmoji }: IMessageFields) => { ({ attachment, getCustomEmoji }: IMessageFields) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { baseUrl, user } = useContext(MessageContext); const { user } = useContext(MessageContext);
if (!attachment.fields) { if (!attachment.fields) {
return null; return null;
@ -97,7 +97,6 @@ const Fields = React.memo(
</Text> </Text>
<Markdown <Markdown
msg={field?.value || ''} msg={field?.value || ''}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}

View File

@ -5,7 +5,7 @@ import { CustomIcon } from '../../../CustomIcon';
import styles from '../../styles'; import styles from '../../styles';
import { useTheme } from '../../../../theme'; import { useTheme } from '../../../../theme';
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread: boolean }) => { const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => {
const { theme } = useTheme(); const { theme } = useTheme();
if (isReadReceiptEnabled && !unread && unread !== null) { if (isReadReceiptEnabled && !unread && unread !== null) {
return <CustomIcon name='check' color={themes[theme].tintColor} size={16} style={styles.rightIcons} />; return <CustomIcon name='check' color={themes[theme].tintColor} size={16} style={styles.rightIcons} />;

View File

@ -17,8 +17,8 @@ interface IRightIcons {
type: MessageType; type: MessageType;
msg?: string; msg?: string;
isEdited: boolean; isEdited: boolean;
isReadReceiptEnabled: boolean; isReadReceiptEnabled?: boolean;
unread: boolean; unread?: boolean;
hasError: boolean; hasError: boolean;
} }
@ -27,7 +27,7 @@ const RightIcons = ({ type, msg, isEdited, hasError, isReadReceiptEnabled, unrea
<Encrypted type={type} /> <Encrypted type={type} />
<Edited testID={`${msg}-edited`} isEdited={isEdited} /> <Edited testID={`${msg}-edited`} isEdited={isEdited} />
<MessageError hasError={hasError} /> <MessageError hasError={hasError} />
<ReadReceipt isReadReceiptEnabled={isReadReceiptEnabled} unread={unread || false} /> <ReadReceipt isReadReceiptEnabled={isReadReceiptEnabled} unread={unread} />
</View> </View>
); );

View File

@ -6,16 +6,17 @@ import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import Markdown, { MarkdownPreview } from '../markdown'; import Markdown, { MarkdownPreview } from '../markdown';
import User from './User'; import User from './User';
import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME, getInfoMessage } from './utils'; import { messageHaveAuthorName, getInfoMessage } from './utils';
import MessageContext from './Context'; import MessageContext from './Context';
import { IMessageContent } from './interfaces'; import { IMessageContent } from './interfaces';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { MessageTypesValues } from '../../definitions';
const Content = React.memo( const Content = React.memo(
(props: IMessageContent) => { (props: IMessageContent) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { baseUrl, user, onLinkPress } = useContext(MessageContext); const { user, onLinkPress } = useContext(MessageContext);
if (props.isInfo) { if (props.isInfo) {
// @ts-ignore // @ts-ignore
@ -26,8 +27,7 @@ const Content = React.memo(
{infoMessage} {infoMessage}
</Text> </Text>
); );
if (messageHaveAuthorName(props.type as MessageTypesValues)) {
if (SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME.includes(props.type)) {
return ( return (
<Text> <Text>
<User {...props} /> {renderMessageContent} <User {...props} /> {renderMessageContent}
@ -54,7 +54,6 @@ const Content = React.memo(
<Markdown <Markdown
msg={props.msg} msg={props.msg}
md={props.md} md={props.md}
baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji} getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption} enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username} username={user.username}
@ -65,6 +64,7 @@ const Content = React.memo(
useRealName={props.useRealName} useRealName={props.useRealName}
theme={theme} theme={theme}
onLinkPress={onLinkPress} onLinkPress={onLinkPress}
isTranslated={props.isTranslated}
/> />
); );
} }

View File

@ -6,11 +6,11 @@ import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { IMessageEmoji } from './interfaces'; import { IMessageEmoji } from './interfaces';
const Emoji = React.memo( const Emoji = React.memo(
({ content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => { ({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => {
const parsedContent = content.replace(/^:|:$/g, ''); const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent); const emoji = getCustomEmoji(parsedContent);
if (emoji) { if (emoji) {
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />; return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
} }
return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>; return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>;
}, },

View File

@ -81,7 +81,6 @@ const ImageContainer = React.memo(
<Markdown <Markdown
msg={file.description} msg={file.description}
style={[isReply && style]} style={[isReply && style]}
baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}

View File

@ -820,8 +820,8 @@ export const SystemMessages = () => (
<Message msg='public' type='room_changed_privacy' isInfo /> <Message msg='public' type='room_changed_privacy' isInfo />
<Message type='room_e2e_disabled' isInfo /> <Message type='room_e2e_disabled' isInfo />
<Message type='room_e2e_enabled' isInfo /> <Message type='room_e2e_enabled' isInfo />
<Message type='removed-user-from-team' isInfo /> <Message msg='rocket.cat' type='removed-user-from-team' isInfo />
<Message type='added-user-to-team' isInfo /> <Message msg='rocket.cat' type='added-user-to-team' isInfo />
<Message type='user-added-room-to-team' isInfo msg='channel-name' /> <Message type='user-added-room-to-team' isInfo msg='channel-name' />
<Message type='user-converted-to-team' isInfo msg='channel-name' /> <Message type='user-converted-to-team' isInfo msg='channel-name' />
<Message type='user-converted-to-channel' isInfo msg='channel-name' /> <Message type='user-converted-to-channel' isInfo msg='channel-name' />

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