├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── example
├── ChatScreen.js
└── ChatScreenStyles.js
├── index.js
├── package.json
├── screens
├── m-zoom-speed-1.gif
├── m-zoom-speed-2.gif
├── m-zoom-speed-3.gif
├── m1.gif
├── m2.gif
└── m3.gif
├── src
├── Avatar
│ ├── AvatarStyles.js
│ └── index.js
├── Editor
│ ├── EditorStyles.js
│ ├── EditorUtils.js
│ └── index.js
├── MentionList
│ ├── MentionListStyles.js
│ └── index.js
└── MentionListItem
│ ├── MentionListItemStyles.js
│ └── index.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | charset = utf-8
13 | trim_trailing_whitespace = true
14 | use-tabs = true
15 | tabWidth = 4
16 |
17 |
18 | [*.gradle]
19 | indent_size = 4
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # xcode
2 | .DS_Store
3 |
4 | #node
5 | node_modules/
6 |
7 | #editor config
8 | # .editorconfig
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Muhammad Raza Dar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-mentions-editor [](https://badge.fury.io/js/react-native-mentions-editor)
2 | Mentions TextInput for React Native. Tested on iOS and should work on Android as well. Because it's a plain Javascript base solution with some react-native TextInput support.
3 | - Inspiration [react-native-mentions](https://github.com/harshq/react-native-mentions)
4 |
5 |
6 | ## Installation
7 |
8 | ```yarn add react-native-mentions-editor```
9 | or
10 | ```npm install --save react-native-mentions-editor```
11 |
12 | ```
13 | If you love this component, give a star, you will be a ray of sunshine :)
14 | ```
15 |
16 | ## Demo
17 |
18 | 
19 | 
20 | 
21 | 
22 | 
23 | 
24 |
25 | ## Usage
26 |
27 | ```js
28 | import Editor, { displayTextWithMentions} from 'react-native-mentions-editor';
29 | const users = [
30 | { "id": 1, "name": "Raza Dar", "username": "mrazadar", "gender": "male"},
31 | { "id": 3, "name": "Atif Rashid", "username": "atif.rashid", "gender": "male"},
32 | { "id": 4, "name": "Peter Pan", "username": "peter.pan", "gender": "male"},
33 | { "id": 5, "name": "John Doe", "username": "john.doe", "gender": "male"},
34 | { "id": 6, "name": "Meesha Shafi", "username": "meesha.shafi", "gender": "female"}
35 | ];
36 |
47 |
48 | const formatMentionNode = (txt, key)=> (
49 |
50 | {txt}
51 |
52 | )
53 |
54 |
55 | {displayTextWithMentions(message.text, formatMentionNode)}
56 |
57 | ```
58 | ## How it works
59 |
60 | This component allows you to @mention anywhere in the input value. (Not possible using [react-native-mentions](https://github.com/harshq/react-native-mentions)).
61 | Work nicely with selection and highlight of text. This component used special mark-up `@[username](id:1)` to differentiate mentions in the input value.
62 | Whenever input value change the `onChange` callback will be called, with an object containing two properties.
63 |
64 | ```js
65 | this.props.onChange({
66 | displayText: text,// displayText: "Hey @mrazadar this is good work"
67 | text: this.formatTextWithMentions(text) //text: "Hey @[mrazadar](id:1) this is good work"
68 | });
69 | ```
70 |
71 | `displayText` Will have raw text user will see on the screen. You can see that in the comment.
72 | `text` Will have formatted text with some markup to parse mentions on the server and other clients. There is a function called `displayTextWithMentions` you can use this function to parse this mark-up with the parser function (Which format the mention node according to formatter function. Check the example app).
73 |
74 | If you want to only parse mentions in the string but don't want to format them you can use this `EditorUtils.findMentions` function to actually parse the mentions in the string.
75 | This will parse special mark-up `@[username](id:1)` and gives you the exact `positions` and `username` and `id` for that mention. Which you can use for tagging / emailing purposes on the server etc.
76 | You can use this function as:
77 |
78 |
79 | ```js
80 | import { EU as EditorUtils } from 'react-native-mentions-editor';
81 | EditorUtils.findMentions("Hey @[mrazadar](id:1) this is good work" );
82 |
83 | //Check the definition of this function
84 | findMentions: (val) => {
85 | /**
86 | * Both Mentions and Selections are 0-th index based in the strings
87 | * meaning their indexes in the string start from 0
88 | * findMentions finds starting and ending positions of mentions in the given text
89 | * @param val string to parse to find mentions
90 | * @returns list of found mentions
91 | */
92 | let reg = /@\[([^\]]+?)\]\(id:([^\]]+?)\)/igm;
93 | let indexes = [];
94 | while (match = reg.exec(val)) {
95 | indexes.push({
96 | start: match.index,
97 | end: (reg.lastIndex-1),
98 | username: match[1],
99 | userId: match[2],
100 | type: EU.specialTagsEnum.mention
101 | });
102 | }
103 | return indexes;
104 | },
105 | ```
106 |
107 | ## Props {property : type}
108 |
109 | **`list: array`** This should be the list of objects to be used as options for the mentions list. **Note** This must contain `id` and `username` properties to uniqely identify object in the list.
110 |
111 | **`initialValue: string`** Use this to initialize TextInput with the initial value. Usage. `initalValue: "Hey @[mrazadar](id:1) this is good work"`
112 |
113 | **`clearInput: bool`** When true input will be clear automatically.
114 |
115 | **`onChange: function`** This function will be called on input change event.
116 |
117 | **`showEditor: bool`** Programmatically show/hide editor by using this property.
118 |
119 | **`toggleEditor: function`** Use this to handle `blur` event on input.
120 |
121 | **`showMentions: bool`** Use this property to programmatically trigger the `mentionsList` this will add `@` character in the value.
122 |
123 | **`onHideMentions: function`** This callback will be called when user stop tracking of mention.
124 |
125 | **`placeholder: string`** placeholder for empty input.
126 |
127 | **`renderMentionList: function`** If you want to render totally different list. You can use this property to provide alternative mention list renderer. It will be called with certain properties to controll the functionality of list.
128 |
129 | ```js
130 | renderMentionList Props: object
131 |
132 | mentionListProps= {
133 | list: props.list, //the default list you passed to this component
134 | keyword: state.keyword, //keyword to filter the list. e.g. `@m`
135 | isTrackingStarted: state.isTrackingStarted, // will be true if user started typing `@`
136 | onSuggestionTap: this.onSuggestionTap.bind(this), //this function should be called once user press on the list item
137 | editorStyles: props.editorStyles, // these will be the props passed to the Editor component.
138 | };
139 |
140 | ```
141 | **`editorStyles: object`** This object will contain the overriding styles for different nodes. Check the below object to see how you can override styles.
142 |
143 | ```js
144 | editorStyles: {
145 | mainContainer: {},
146 | editorContainer: {...},
147 | inputMaskTextWrapper: {},
148 | inputMaskText: {},
149 | input: {},
150 | mentionsListWrapper:{},
151 | mentionListItemWrapper: {}
152 | mentionListItemTextWrapper: {},
153 | mentionListItemTitle: {}
154 | mentionListItemUsername: {}
155 | }
156 | ```
157 |
158 | ## Example
159 |
160 | Check out the full example in [example](https://github.com/mrazadar/react-native-mentions-editor/tree/master/example) folder
161 |
162 | ## License
163 |
164 | [MIT License](http://opensource.org/licenses/mit-license.html). © Muhammad Raza Dar
--------------------------------------------------------------------------------
/example/ChatScreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import {
3 | ScrollView,
4 | Text,
5 | KeyboardAvoidingView,
6 | View,
7 | TouchableOpacity,
8 | FlatList
9 | } from "react-native";
10 | // Add Actions - replace 'Your' with whatever your reducer is called :)
11 | // import YourActions from '../Redux/YourRedux'
12 |
13 | import Editor, { EU } from "react-native-mentions-editor";
14 |
15 | // Styles
16 | import styles from "./Styles/ChatScreenStyles";
17 |
18 | const users = [
19 | { id: 1, name: "Raza Dar", username: "mrazadar", gender: "male" },
20 | { id: 3, name: "Atif Rashid", username: "atif.rashid", gender: "male" },
21 | { id: 4, name: "Peter Pan", username: "peter.pan", gender: "male" },
22 | { id: 5, name: "John Doe", username: "john.doe", gender: "male" },
23 | { id: 6, name: "Meesha Shafi", username: "meesha.shafi", gender: "female" }
24 | ];
25 |
26 | const formatMentionNode = (txt, key) => (
27 |
28 | {txt}
29 |
30 | );
31 |
32 | class ChatScreen extends Component {
33 | constructor(props) {
34 | super(props);
35 | this.state = {
36 | initialValue:
37 | "Hey @[mrazadar](id:1) this is good work. Tell @[john.doe](id:5) to use this package.",
38 | showEditor: true,
39 | message: null,
40 | messages: [],
41 | clearInput: false,
42 | showMentions: false /**use this parameter to programmatically trigger the mentionsList */
43 | };
44 | }
45 | onChangeHandler = message => {
46 | /**
47 | * this callback will be called whenever input value change and will have
48 | * formatted value for mentioned syntax
49 | * @message : {text: 'Hey @(mrazadar)(id:1) this is good work.', displayText: `Hey @mrazadar this is good work`}
50 | * */
51 |
52 | this.setState({
53 | message,
54 | clearInput: false
55 | });
56 | };
57 | sendMessage = () => {
58 | if (!this.state.message) return;
59 | const messages = [this.state.message, ...this.state.messages];
60 | this.setState({
61 | messages,
62 | message: null,
63 | clearInput: true
64 | });
65 | };
66 |
67 | toggleEditor = () => {
68 | /**
69 | * This callback will be called
70 | * once user left the input field.
71 | * This will handle blur event.
72 | */
73 | // this.setState({
74 | // showEditor: false,
75 | // })
76 | };
77 |
78 | onHideMentions = () => {
79 | /**
80 | * This callback will be called
81 | * When MentionsList hide due to any user change
82 | */
83 | this.setState({
84 | showMentions: false
85 | });
86 | };
87 |
88 | renderMessageListItem({ item: message, index }) {
89 | return (
90 |
91 |
92 | {EU.displayTextWithMentions(message.text, formatMentionNode)}
93 |
94 |
95 | );
96 | }
97 | renderMessageList() {
98 | return (
99 | `${message.text}-${index}`}
107 | renderItem={rowData => {
108 | return this.renderMessageListItem(rowData);
109 | }}
110 | />
111 | );
112 | }
113 |
114 | render() {
115 | return (
116 |
117 |
118 |
119 |
120 | React-Native Mentions Package
121 | Built by @mrazadar
122 |
123 |
124 | {this.renderMessageList()}
125 |
126 |
127 |
138 |
142 | Send
143 |
144 |
145 |
146 |
147 |
148 | );
149 | }
150 | }
151 |
152 | export default ChatScreen;
153 |
--------------------------------------------------------------------------------
/example/ChatScreenStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Dimensions } from "react-native";
2 | import { ApplicationStyles } from "../../Themes/";
3 | // import Colors from '../../Themes/Colors'
4 |
5 | const { width, height } = Dimensions.get("window");
6 | const screenWidth = width < height ? width : height;
7 | const screenHeight = width < height ? height : width;
8 |
9 | export default StyleSheet.create({
10 | ...ApplicationStyles.screen,
11 | main: {
12 | flex: 1,
13 |
14 | backgroundColor: "#fff",
15 | height: screenHeight,
16 |
17 | marginTop: 100
18 | },
19 | container: {
20 | height: screenHeight,
21 |
22 | alignItems: "center",
23 | justifyContent: "space-between"
24 | },
25 | header: {
26 | // height: 200,
27 | },
28 | heading: {
29 | fontSize: 24,
30 | fontWeight: "bold"
31 | // color: 'green'
32 | },
33 | sub: {
34 | color: "rgba(0, 0, 0, 0.4)",
35 | fontSize: 12,
36 | textAlign: "center"
37 | },
38 | messageList: {
39 | paddingVertical: 50
40 | },
41 | messageText: {},
42 |
43 | footer: {
44 | backgroundColor: "lightgreen",
45 | height: 200,
46 | width: screenWidth,
47 | flexDirection: "row",
48 | justifyContent: "space-between",
49 | alignItems: "center",
50 | marginBottom: 100,
51 | padding: 15
52 | },
53 | sendBtn: {
54 | width: 50,
55 | height: 40,
56 | backgroundColor: "green",
57 | borderRadius: 6,
58 | marginLeft: 5,
59 | justifyContent: "center",
60 | textAlign: "center"
61 | },
62 | sendBtnText: {
63 | fontSize: 18,
64 | fontWeight: "bold",
65 | color: "#fff",
66 | textAlign: "center"
67 | },
68 | mention: {
69 | fontSize: 16,
70 | fontWeight: "400",
71 | backgroundColor: "rgba(36, 77, 201, 0.05)",
72 | color: "#244dc9"
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import Editor from './src/Editor';
2 | export * from './src/Editor/EditorUtils';
3 | export default Editor;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-mentions-editor",
3 | "version": "1.0.11",
4 | "description": "Mentions for React-Native. Tested on iOS. Should work on Andriod.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mrazadar/react-native-mentions-editor.git"
12 | },
13 | "keywords": [
14 | "react-native-mentions",
15 | "react native mentions",
16 | "react-native",
17 | "react-component",
18 | "editor",
19 | "mobile",
20 | "ios",
21 | "android",
22 | "mentions"
23 | ],
24 | "author": "Muhammad Raza Dar (https://about.me/mrazadar)",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/mrazadar/react-native-mentions-editor/issues"
28 | },
29 | "homepage": "https://github.com/mrazadar/react-native-mentions-editor#readme"
30 | }
31 |
--------------------------------------------------------------------------------
/screens/m-zoom-speed-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m-zoom-speed-1.gif
--------------------------------------------------------------------------------
/screens/m-zoom-speed-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m-zoom-speed-2.gif
--------------------------------------------------------------------------------
/screens/m-zoom-speed-3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m-zoom-speed-3.gif
--------------------------------------------------------------------------------
/screens/m1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m1.gif
--------------------------------------------------------------------------------
/screens/m2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m2.gif
--------------------------------------------------------------------------------
/screens/m3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrazadar/react-native-mentions-editor/ea1fe89d2e80e80ec46ebef3f1b4e73407bb2a2a/screens/m3.gif
--------------------------------------------------------------------------------
/src/Avatar/AvatarStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 |
3 | export default StyleSheet.create({
4 | wrapper: {
5 | width: 40,
6 | height: 40,
7 | borderRadius: 50,
8 | marginRight: 5,
9 | alignItems: "center",
10 | justifyContent: "center"
11 | },
12 | name: {
13 | fontWeight: "bold",
14 | fontSize: 16,
15 | color: "#fff"
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/src/Avatar/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { View, Text } from "react-native";
5 |
6 | import styles from "./AvatarStyles";
7 |
8 | const getFirstChar = str => str.charAt(0).toUpperCase();
9 |
10 | const alphabetColors = [
11 | "#FFD552",
12 | "#ffca0b",
13 | "#9C0D05",
14 | "#E1DB00",
15 | "#E99600",
16 | "#E1DB00",
17 | "#06BC0C",
18 | "#06BCAE",
19 | "#0695BC",
20 | "#0660BC",
21 | "#3006BC",
22 | "#6606BC",
23 | "#c31616",
24 | "#BC0680",
25 | "#BC0642",
26 | "#BC3406",
27 | "#BCA106",
28 | "#535322",
29 | "#497724",
30 | "#929292",
31 | "#606060",
32 | "#262626",
33 | "#7B9FAB",
34 | "#1393BD",
35 | "#5E13BD",
36 | "#E208A7"
37 | ];
38 |
39 | const UserThumbnail = props => {
40 | const { user } = props;
41 | let name = user && user.name;
42 | if (!name || name === "") {
43 | if (user && user.first_name && user.last_name) {
44 | name = `${user.first_name} ${user.last_name}`;
45 | } else {
46 | return null;
47 | }
48 | }
49 | const text = getFirstChar(name);
50 | const bgIndex = Math.floor(text.charCodeAt(0) % alphabetColors.length);
51 | const bgColor = alphabetColors[bgIndex];
52 |
53 | const thumbnail = (
54 |
61 | {`${text}`}
62 |
63 | );
64 | return thumbnail;
65 | };
66 |
67 | UserThumbnail.propTypes = {
68 | user: PropTypes.object,
69 | wrapperStyles: PropTypes.object,
70 | charStyles: PropTypes.object,
71 | to: PropTypes.string
72 | };
73 |
74 | export default UserThumbnail;
75 |
--------------------------------------------------------------------------------
/src/Editor/EditorStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | backgroundColor: "#fff",
6 | borderColor: "green",
7 | borderWidth: 1,
8 | width: 300
9 | },
10 | textContainer: {
11 | alignSelf: "stretch",
12 | position: "relative",
13 | minHeight: 40,
14 | maxHeight: 140
15 | },
16 | input: {
17 | fontSize: 16,
18 | color: "#000",
19 | fontWeight: "400",
20 | paddingHorizontal: 20,
21 | minHeight: 40,
22 | position: "absolute",
23 | top: 0,
24 | color: "transparent",
25 | alignSelf: "stretch",
26 | width: "100%"
27 | },
28 | formmatedTextWrapper: {
29 | minHeight: 40,
30 | position: "absolute",
31 | top: 0,
32 | paddingHorizontal: 20,
33 | paddingVertical: 5,
34 | width: "100%"
35 | },
36 | formmatedText: {
37 | fontSize: 16,
38 | fontWeight: "400"
39 | },
40 | mention: {
41 | fontSize: 16,
42 | fontWeight: "400",
43 | backgroundColor: "rgba(36, 77, 201, 0.05)",
44 | color: "#244dc9"
45 | },
46 | placeholderText: {
47 | color: "rgba(0, 0, 0, 0.1)",
48 | fontSize: 16
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/src/Editor/EditorUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * EditorUtils contains helper
3 | * functions for our Editor
4 | */
5 |
6 | export const displayTextWithMentions = (inputText, formatMentionNode) => {
7 | /**
8 | * Use this function to parse mentions markup @[username](id) in the string value.
9 | */
10 | if (inputText === "") return null;
11 | const retLines = inputText.split("\n");
12 | const formattedText = [];
13 | retLines.forEach((retLine, rowIndex) => {
14 | const mentions = EU.findMentions(retLine);
15 | if (mentions.length) {
16 | let lastIndex = 0;
17 | mentions.forEach((men, index) => {
18 | const initialStr = retLine.substring(lastIndex, men.start);
19 | lastIndex = men.end + 1;
20 | formattedText.push(initialStr);
21 | const formattedMention = formatMentionNode(
22 | `@${men.username}`,
23 | `${index}-${men.id}-${rowIndex}`
24 | );
25 | formattedText.push(formattedMention);
26 | if (mentions.length - 1 === index) {
27 | const lastStr = retLine.substr(lastIndex); //remaining string
28 | formattedText.push(lastStr);
29 | }
30 | });
31 | } else {
32 | formattedText.push(retLine);
33 | }
34 | formattedText.push("\n");
35 | });
36 | return formattedText;
37 | };
38 |
39 | export const EU = {
40 | specialTagsEnum: {
41 | mention: "mention",
42 | strong: "strong",
43 | italic: "italic",
44 | underline: "underline"
45 | },
46 | isKeysAreSame: (src, dest) => src.toString() === dest.toString(),
47 | getLastItemInMap: map => Array.from(map)[map.size - 1],
48 | getLastKeyInMap: map => Array.from(map.keys())[map.size - 1],
49 | getLastValueInMap: map => Array.from(map.values())[map.size - 1],
50 | updateRemainingMentionsIndexes: (map, { start, end }, diff, shouldAdd) => {
51 | var newMap = new Map(map);
52 | const keys = EU.getSelectedMentionKeys(newMap, { start, end });
53 | keys.forEach(key => {
54 | const newKey = shouldAdd
55 | ? [key[0] + diff, key[1] + diff]
56 | : [key[0] - diff, key[1] - diff];
57 | const value = newMap.get(key);
58 | newMap.delete(key);
59 | //ToDo+ push them in the same order.
60 | newMap.set(newKey, value);
61 | });
62 | return newMap;
63 | },
64 | getSelectedMentionKeys: (map, { start, end }) => {
65 | // mention [2, 5],
66 | // selection [3, 6]
67 | const mantionKeys = [...map.keys()];
68 | const keys = mantionKeys.filter(
69 | ([a, b]) => EU.between(a, start, end) || EU.between(b, start, end)
70 | );
71 | return keys;
72 | },
73 | findMentionKeyInMap: (map, cursorIndex) => {
74 | // const keys = Array.from(map.keys())
75 | // OR
76 | const keys = [...map.keys()];
77 | const key = keys.filter(([a, b]) => EU.between(cursorIndex, a, b))[0];
78 | return key;
79 | },
80 | addMenInSelection: (selection, prevSelc, mentions) => {
81 | /**
82 | * Both Mentions and Selections are 0-th index based in the strings
83 | * meaning their indexes in the string start from 0
84 | * While user made a selection automatically add mention in the selection.
85 | */
86 | const sel = { ...selection };
87 | mentions.forEach((value, [menStart, menEnd]) => {
88 | if (EU.diff(prevSelc.start, prevSelc.end) < EU.diff(sel.start, sel.end)) {
89 | //user selecting.
90 | if (EU.between(sel.start, menStart, menEnd)) {
91 | //move sel to the start of mention
92 | sel.start = menStart; //both men and selection is 0th index
93 | }
94 | if (EU.between(sel.end - 1, menStart, menEnd)) {
95 | //move sel to the end of mention
96 | sel.end = menEnd + 1;
97 | }
98 | } else {
99 | //previousSelection.diff > currentSelection.diff //user deselecting.
100 | if (EU.between(sel.start, menStart, menEnd)) {
101 | //deselect mention to the end of mention
102 | sel.start = menEnd + 1;
103 | }
104 | if (EU.between(sel.end, menStart, menEnd)) {
105 | //deselect mention to the start of mention
106 | sel.end = menStart;
107 | }
108 | }
109 | });
110 | return sel;
111 | },
112 | moveCursorToMentionBoundry: (
113 | selection,
114 | prevSelc,
115 | mentions,
116 | isTrackingStarted
117 | ) => {
118 | /**
119 | * Both Mentions and Selections are 0-th index based in the strings
120 | * moveCursorToMentionBoundry will move cursor to the start
121 | * or to the end of mention based on user traverse direction.
122 | */
123 |
124 | const sel = { ...selection };
125 | if (isTrackingStarted) return sel;
126 | mentions.forEach((value, [menStart, menEnd]) => {
127 | if (prevSelc.start > sel.start) {
128 | //traversing Right -to- Left <=
129 | if (EU.between(sel.start, menStart, menEnd)) {
130 | //move cursor to the start of mention
131 | sel.start = menStart;
132 | sel.end = menStart;
133 | }
134 | } else {
135 | //traversing Left -to- Right =>
136 | if (EU.between(sel.start - 1, menStart, menEnd)) {
137 | //move cursor to the end of selection
138 | sel.start = menEnd + 1;
139 | sel.end = menEnd + 1;
140 | }
141 | }
142 | });
143 | return sel;
144 | },
145 | between: (x, min, max) => x >= min && x <= max,
146 | sum: (x, y) => x + y,
147 | diff: (x, y) => Math.abs(x - y),
148 | isEmpty: str => str === "",
149 | getMentionsWithInputText: inputText => {
150 | /**
151 | * translate provided string e.g. `Hey @[mrazadar](id:1) this is good work.`
152 | * populate mentions map with [start, end] : {...user}
153 | * translate inputText to desired format; `Hey @mrazadar this is good work.`
154 | */
155 |
156 | const map = new Map();
157 | let newValue = "";
158 |
159 | if (inputText === "") return null;
160 | const retLines = inputText.split("\n");
161 |
162 | retLines.forEach((retLine, rowIndex) => {
163 | const mentions = EU.findMentions(retLine);
164 | if (mentions.length) {
165 | let lastIndex = 0;
166 | let endIndexDiff = 0;
167 | mentions.forEach((men, index) => {
168 | newValue = newValue.concat(retLine.substring(lastIndex, men.start));
169 | const username = `@${men.username}`;
170 | newValue = newValue.concat(username);
171 | const menEndIndex = men.start + (username.length - 1);
172 | map.set([men.start - endIndexDiff, menEndIndex - endIndexDiff], {
173 | id: men.id,
174 | username: men.username
175 | });
176 | //indexes diff with the new formatted string.
177 | endIndexDiff = endIndexDiff + Math.abs(men.end - menEndIndex);
178 | //update last index
179 | lastIndex = men.end + 1;
180 | if (mentions.length - 1 === index) {
181 | const lastStr = retLine.substr(lastIndex); //remaining string
182 | newValue = newValue.concat(lastStr);
183 | }
184 | });
185 | } else {
186 | newValue = newValue.concat(retLine);
187 | }
188 | if (rowIndex !== retLines.length - 1) {
189 | newValue = newValue.concat("\n");
190 | }
191 | });
192 | return {
193 | map,
194 | newValue
195 | };
196 | },
197 | findMentions: val => {
198 | /**
199 | * Both Mentions and Selections are 0-th index based in the strings
200 | * meaning their indexes in the string start from 0
201 | * findMentions finds starting and ending positions of mentions in the given text
202 | * @param val string to parse to find mentions
203 | * @returns list of found mentions
204 | */
205 | let reg = /@\[([^\]]+?)\]\(id:([^\]]+?)\)/gim;
206 | let indexes = [];
207 | while ((match = reg.exec(val))) {
208 | indexes.push({
209 | start: match.index,
210 | end: reg.lastIndex - 1,
211 | username: match[1],
212 | id: match[2],
213 | type: EU.specialTagsEnum.mention
214 | });
215 | }
216 | return indexes;
217 | },
218 | whenTrue: (next, current, key) => {
219 | /**
220 | * whenTrue function will be used to check the
221 | * boolean props for the component
222 | * @params {current, next, key}
223 | * @next: this.props
224 | * @current: nextProps
225 | * @key: key to lookup in both objects
226 | * and will only returns true. if nextProp is true
227 | * and nextProp is a different version/value from
228 | * previous prop
229 | */
230 | return next[key] && next[key] !== current[key];
231 | },
232 | displayTextWithMentions: displayTextWithMentions
233 | };
234 |
235 | export default EU;
236 |
--------------------------------------------------------------------------------
/src/Editor/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import {
5 | View,
6 | TextInput,
7 | Text,
8 | Animated,
9 | Platform,
10 | ScrollView
11 | } from "react-native";
12 |
13 | import EU from "./EditorUtils";
14 | import styles from "./EditorStyles";
15 | import MentionList from "../MentionList";
16 |
17 | export class Editor extends React.Component {
18 | static propTypes = {
19 | list: PropTypes.array,
20 | initialValue: PropTypes.string,
21 | clearInput: PropTypes.bool,
22 | onChange: PropTypes.func,
23 | showEditor: PropTypes.bool,
24 | toggleEditor: PropTypes.func,
25 | showMentions: PropTypes.bool,
26 | onHideMentions: PropTypes.func,
27 | editorStyles: PropTypes.object,
28 | placeholder: PropTypes.string,
29 | renderMentionList: PropTypes.func
30 | };
31 |
32 | constructor(props) {
33 | super(props);
34 | this.mentionsMap = new Map();
35 | let msg = "";
36 | let formattedMsg = "";
37 | if (props.initialValue && props.initialValue !== "") {
38 | const { map, newValue } = EU.getMentionsWithInputText(props.initialValue);
39 | this.mentionsMap = map;
40 | msg = newValue;
41 | formattedMsg = this.formatText(newValue);
42 | setTimeout(()=>{
43 | this.sendMessageToFooter(newValue);
44 | });
45 | }
46 | this.state = {
47 | clearInput: props.clearInput,
48 | inputText: msg,
49 | formattedText: formattedMsg,
50 | keyword: "",
51 | textInputHeight: "",
52 | isTrackingStarted: false,
53 | suggestionRowHeight: new Animated.Value(0),
54 | triggerLocation: "anywhere", //'new-words-only', //anywhere
55 | trigger: "@",
56 | selection: {
57 | start: 0,
58 | end: 0
59 | },
60 | menIndex: 0,
61 | showMentions: false,
62 | editorHeight: 72,
63 | scrollContentInset: { top: 0, bottom: 0, left: 0, right: 0 },
64 | placeholder: props.placeholder || "Type something..."
65 | };
66 | this.isTrackingStarted = false;
67 | this.previousChar = " ";
68 | }
69 | static getDerivedStateFromProps(nextProps, prevState) {
70 | if (nextProps.clearInput !== prevState.clearInput) {
71 | return { clearInput: nextProps.clearInput };
72 | }
73 |
74 | if (nextProps.showMentions && !prevState.showMentions) {
75 | const newInputText = `${prevState.inputText}${prevState.trigger}`;
76 | return {
77 | inputText: newInputText,
78 | showMentions: nextProps.showMentions
79 | };
80 | }
81 |
82 | if (!nextProps.showMentions) {
83 | return {
84 | showMentions: nextProps.showMentions
85 | };
86 | }
87 | return null;
88 | }
89 |
90 | componentDidUpdate(prevProps, prevState) {
91 | // only update chart if the data has changed
92 | if (this.state.inputText !== "" && this.state.clearInput) {
93 | this.setState({
94 | inputText: "",
95 | formattedText: ""
96 | });
97 | this.mentionsMap.clear();
98 | }
99 |
100 | if (EU.whenTrue(this.props, prevProps, "showMentions")) {
101 | //don't need to close on false; user show select it.
102 | this.onChange(this.state.inputText, true);
103 | }
104 | }
105 |
106 | updateMentionsMap(selection, count, shouldAdd) {
107 | this.mentionsMap = EU.updateRemainingMentionsIndexes(
108 | this.mentionsMap,
109 | selection,
110 | count,
111 | shouldAdd
112 | );
113 | }
114 |
115 | startTracking(menIndex) {
116 | this.isTrackingStarted = true;
117 | this.menIndex = menIndex;
118 | this.setState({
119 | keyword: "",
120 | menIndex,
121 | isTrackingStarted: true
122 | });
123 | }
124 |
125 | stopTracking() {
126 | this.isTrackingStarted = false;
127 | // this.closeSuggestionsPanel();
128 | this.setState({
129 | isTrackingStarted: false
130 | });
131 | this.props.onHideMentions();
132 | }
133 |
134 | updateSuggestions(lastKeyword) {
135 | this.setState({
136 | keyword: lastKeyword
137 | });
138 | }
139 |
140 | resetTextbox() {
141 | this.previousChar = " ";
142 | this.stopTracking();
143 | this.setState({ textInputHeight: this.props.textInputMinHeight });
144 | }
145 |
146 | identifyKeyword(inputText) {
147 | /**
148 | * filter the mentions list
149 | * according to what user type with
150 | * @ char e.g. @billroy
151 | */
152 | if (this.isTrackingStarted) {
153 | let pattern = null;
154 | if (this.state.triggerLocation === "new-word-only") {
155 | pattern = new RegExp(
156 | `\\B${this.state.trigger}[a-z0-9_-]+|\\B${this.state.trigger}`,
157 | `gi`
158 | );
159 | } else {
160 | //anywhere
161 | pattern = new RegExp(
162 | `\\${this.state.trigger}[a-z0-9_-]+|\\${this.state.trigger}`,
163 | `i`
164 | );
165 | }
166 | const str = inputText.substr(this.menIndex);
167 | const keywordArray = str.match(pattern);
168 | if (keywordArray && !!keywordArray.length) {
169 | const lastKeyword = keywordArray[keywordArray.length - 1];
170 | this.updateSuggestions(lastKeyword);
171 | }
172 | }
173 | }
174 |
175 | checkForMention(inputText, selection) {
176 | /**
177 | * Open mentions list if user
178 | * start typing @ in the string anywhere.
179 | */
180 | const menIndex = selection.start - 1;
181 | // const lastChar = inputText.substr(inputText.length - 1);
182 | const lastChar = inputText.substr(menIndex, 1);
183 | const wordBoundry =
184 | this.state.triggerLocation === "new-word-only"
185 | ? this.previousChar.trim().length === 0
186 | : true;
187 | if (lastChar === this.state.trigger && wordBoundry) {
188 | this.startTracking(menIndex);
189 | } else if (lastChar.trim() === "" && this.state.isTrackingStarted) {
190 | this.stopTracking();
191 | }
192 | this.previousChar = lastChar;
193 | this.identifyKeyword(inputText);
194 | }
195 |
196 | getInitialAndRemainingStrings(inputText, menIndex) {
197 | /**
198 | * extractInitialAndRemainingStrings
199 | * this function extract the initialStr and remainingStr
200 | * at the point of new Mention string.
201 | * Also updates the remaining string if there
202 | * are any adjcent mentions text with the new one.
203 | */
204 | // const {inputText, menIndex} = this.state;
205 | let initialStr = inputText.substr(0, menIndex).trim();
206 | if (!EU.isEmpty(initialStr)) {
207 | initialStr = initialStr + " ";
208 | }
209 | /**
210 | * remove the characters adjcent with @ sign
211 | * and extract the remaining part
212 | */
213 | let remStr =
214 | inputText
215 | .substr(menIndex + 1)
216 | .replace(/\s+/, "\x01")
217 | .split("\x01")[1] || "";
218 |
219 | /**
220 | * check if there are any adjecent mentions
221 | * subtracted in current selection.
222 | * add the adjcent mentions
223 | * @tim@nic
224 | * add nic back
225 | */
226 | const adjMentIndexes = {
227 | start: initialStr.length - 1,
228 | end: inputText.length - remStr.length - 1
229 | };
230 | const mentionKeys = EU.getSelectedMentionKeys(
231 | this.mentionsMap,
232 | adjMentIndexes
233 | );
234 | mentionKeys.forEach(key => {
235 | remStr = `@${this.mentionsMap.get(key).username} ${remStr}`;
236 | });
237 | return {
238 | initialStr,
239 | remStr
240 | };
241 | }
242 |
243 | onSuggestionTap = user => {
244 | /**
245 | * When user select a mention.
246 | * Add a mention in the string.
247 | * Also add a mention in the map
248 | */
249 | const { inputText, menIndex } = this.state;
250 | const { initialStr, remStr } = this.getInitialAndRemainingStrings(
251 | inputText,
252 | menIndex
253 | );
254 |
255 | const username = `@${user.username}`;
256 | const text = `${initialStr}${username} ${remStr}`;
257 | //'@[__display__](__id__)' ///find this trigger parsing from react-mentions
258 |
259 | //set the mentions in the map.
260 | const menStartIndex = initialStr.length;
261 | const menEndIndex = menStartIndex + (username.length - 1);
262 |
263 | this.mentionsMap.set([menStartIndex, menEndIndex], user);
264 |
265 | // update remaining mentions indexes
266 | let charAdded = Math.abs(text.length - inputText.length);
267 | this.updateMentionsMap(
268 | {
269 | start: menEndIndex + 1,
270 | end: text.length
271 | },
272 | charAdded,
273 | true
274 | );
275 |
276 | this.setState({
277 | inputText: text,
278 | formattedText: this.formatText(text)
279 | });
280 | this.stopTracking();
281 | this.sendMessageToFooter(text);
282 | };
283 |
284 | handleSelectionChange = ({ nativeEvent: { selection } }) => {
285 | const prevSelc = this.state.selection;
286 | let newSelc = { ...selection };
287 | if (newSelc.start !== newSelc.end) {
288 | /**
289 | * if user make or remove selection
290 | * Automatically add or remove mentions
291 | * in the selection.
292 | */
293 | newSelc = EU.addMenInSelection(newSelc, prevSelc, this.mentionsMap);
294 | }
295 | // else{
296 | /**
297 | * Update cursor to not land on mention
298 | * Automatically skip mentions boundry
299 | */
300 | // setTimeout(()=>{
301 |
302 | // })
303 | // newSelc = EU.moveCursorToMentionBoundry(newSelc, prevSelc, this.mentionsMap, this.isTrackingStarted);
304 | // }
305 | this.setState({ selection: newSelc });
306 | };
307 |
308 | formatMentionNode = (txt, key) => (
309 |
310 | {txt}
311 |
312 | );
313 |
314 | formatText(inputText) {
315 | /**
316 | * Format the Mentions
317 | * and display them with
318 | * the different styles
319 | */
320 | if (inputText === "" || !this.mentionsMap.size) return inputText;
321 | const formattedText = [];
322 | let lastIndex = 0;
323 | this.mentionsMap.forEach((men, [start, end]) => {
324 | const initialStr =
325 | start === 1 ? "" : inputText.substring(lastIndex, start);
326 | lastIndex = end + 1;
327 | formattedText.push(initialStr);
328 | const formattedMention = this.formatMentionNode(
329 | `@${men.username}`,
330 | `${start}-${men.id}-${end}`
331 | );
332 | formattedText.push(formattedMention);
333 | if (
334 | EU.isKeysAreSame(EU.getLastKeyInMap(this.mentionsMap), [start, end])
335 | ) {
336 | const lastStr = inputText.substr(lastIndex); //remaining string
337 | formattedText.push(lastStr);
338 | }
339 | });
340 | return formattedText;
341 | }
342 |
343 | formatTextWithMentions(inputText) {
344 | if (inputText === "" || !this.mentionsMap.size) return inputText;
345 | let formattedText = "";
346 | let lastIndex = 0;
347 | this.mentionsMap.forEach((men, [start, end]) => {
348 | const initialStr =
349 | start === 1 ? "" : inputText.substring(lastIndex, start);
350 | lastIndex = end + 1;
351 | formattedText = formattedText.concat(initialStr);
352 | formattedText = formattedText.concat(`@[${men.username}](id:${men.id})`);
353 | if (
354 | EU.isKeysAreSame(EU.getLastKeyInMap(this.mentionsMap), [start, end])
355 | ) {
356 | const lastStr = inputText.substr(lastIndex); //remaining string
357 | formattedText = formattedText.concat(lastStr);
358 | }
359 | });
360 | return formattedText;
361 | }
362 |
363 | sendMessageToFooter(text) {
364 | this.props.onChange({
365 | displayText: text,
366 | text: this.formatTextWithMentions(text)
367 | });
368 | }
369 |
370 | onChange = (inputText, fromAtBtn) => {
371 | let text = inputText;
372 | const prevText = this.state.inputText;
373 | let selection = { ...this.state.selection };
374 | if (fromAtBtn) {
375 | //update selection but don't set in state
376 | //it will be auto set by input
377 | selection.start = selection.start + 1;
378 | selection.end = selection.end + 1;
379 | }
380 | if (text.length < prevText.length) {
381 | /**
382 | * if user is back pressing and it
383 | * deletes the mention remove it from
384 | * actual string.
385 | */
386 |
387 | let charDeleted = Math.abs(text.length - prevText.length);
388 | const totalSelection = {
389 | start: selection.start,
390 | end: charDeleted > 1 ? selection.start + charDeleted : selection.start
391 | };
392 | /**
393 | * REmove all the selected mentions
394 | */
395 | if (totalSelection.start === totalSelection.end) {
396 | //single char deleting
397 | const key = EU.findMentionKeyInMap(
398 | this.mentionsMap,
399 | totalSelection.start
400 | );
401 | if (key && key.length) {
402 | this.mentionsMap.delete(key);
403 | /**
404 | * don't need to worry about multi-char selection
405 | * because our selection automatically select the
406 | * whole mention string.
407 | */
408 | const initial = text.substring(0, key[0]); //mention start index
409 | text = initial + text.substr(key[1]); // mentions end index
410 | charDeleted = charDeleted + Math.abs(key[0] - key[1]); //1 is already added in the charDeleted
411 | // selection = {
412 | // start: ((charDeleted+selection.start)-1),
413 | // end: ((charDeleted+selection.start)-1)
414 | // }
415 | this.mentionsMap.delete(key);
416 | }
417 | } else {
418 | //multi-char deleted
419 | const mentionKeys = EU.getSelectedMentionKeys(
420 | this.mentionsMap,
421 | totalSelection
422 | );
423 | mentionKeys.forEach(key => {
424 | this.mentionsMap.delete(key);
425 | });
426 | }
427 | /**
428 | * update indexes on charcters remove
429 | * no need to worry about totalSelection End.
430 | * We already removed deleted mentions from the actual string.
431 | * */
432 | this.updateMentionsMap(
433 | {
434 | start: selection.end,
435 | end: prevText.length
436 | },
437 | charDeleted,
438 | false
439 | );
440 | } else {
441 | //update indexes on new charcter add
442 |
443 | let charAdded = Math.abs(text.length - prevText.length);
444 | this.updateMentionsMap(
445 | {
446 | start: selection.end,
447 | end: text.length
448 | },
449 | charAdded,
450 | true
451 | );
452 | /**
453 | * if user type anything on the mention
454 | * remove the mention from the mentions array
455 | * */
456 | if (selection.start === selection.end) {
457 | const key = EU.findMentionKeyInMap(
458 | this.mentionsMap,
459 | selection.start - 1
460 | );
461 | if (key && key.length) {
462 | this.mentionsMap.delete(key);
463 | }
464 | }
465 | }
466 |
467 | this.setState({
468 | inputText: text,
469 | formattedText: this.formatText(text)
470 | // selection,
471 | });
472 | this.checkForMention(text, selection);
473 | // const text = `${initialStr} @[${user.username}](id:${user.id}) ${remStr}`; //'@[__display__](__id__)' ///find this trigger parsing from react-mentions
474 |
475 | this.sendMessageToFooter(text);
476 | };
477 |
478 | onContentSizeChange = evt => {
479 | /**
480 | * this function will dynamically
481 | * calculate editor height w.r.t
482 | * the size of text in the input.
483 | */
484 | if (evt) {
485 | // const iosTextHeight = 20.5
486 | const androidTextHeight = 20.5;
487 | // const textHeight = Platform.OS === 'ios' ? iosTextHeight : androidTextHeight
488 |
489 | const height =
490 | Platform.OS === "ios"
491 | ? evt.nativeEvent.contentSize.height
492 | : evt.nativeEvent.contentSize.height - androidTextHeight;
493 | let editorHeight = 40;
494 | editorHeight = editorHeight + height;
495 | this.setState({
496 | editorHeight
497 | });
498 | }
499 | };
500 |
501 | render() {
502 | const { props, state } = this;
503 | const { editorStyles = {} } = props;
504 |
505 | if (!props.showEditor) return null;
506 |
507 | const mentionListProps = {
508 | list: props.list,
509 | keyword: state.keyword,
510 | isTrackingStarted: state.isTrackingStarted,
511 | onSuggestionTap: this.onSuggestionTap.bind(this),
512 | editorStyles
513 | };
514 |
515 | return (
516 |
517 | {props.renderMentionList ? (
518 | props.renderMentionList(mentionListProps)
519 | ) : (
520 |
527 | )}
528 |
529 | {
531 | this.scroll = scroll;
532 | }}
533 | onContentSizeChange={() => {
534 | this.scroll.scrollToEnd({ animated: true });
535 | }}
536 | style={[styles.editorContainer, editorStyles.editorContainer]}
537 | >
538 |
539 |
545 | {state.formattedText !== "" ? (
546 |
549 | {state.formattedText}
550 |
551 | ) : (
552 |
558 | {state.placeholder}
559 |
560 | )}
561 |
562 | props.onRef && props.onRef(input)}
564 | style={[styles.input, editorStyles.input]}
565 | multiline
566 | autoFocus
567 | numberOfLines={100}
568 | name={"message"}
569 | value={state.inputText}
570 | onBlur={props.toggleEditor}
571 | onChangeText={this.onChange}
572 | selection={this.state.selection}
573 | selectionColor={"#000"}
574 | onSelectionChange={this.handleSelectionChange}
575 | placeholder={state.placeholder}
576 | onContentSizeChange={this.onContentSizeChange}
577 | scrollEnabled={false}
578 | />
579 |
580 |
581 |
582 |
583 | );
584 | }
585 | }
586 |
587 | export default Editor;
588 |
--------------------------------------------------------------------------------
/src/MentionList/MentionListStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | // flex:1,
6 | maxHeight: 300
7 | },
8 | suggestionsPanelStyle: {},
9 | loaderContainer: {},
10 | mentionsListContainer: {
11 | height: 100
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/src/MentionList/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { ActivityIndicator, FlatList, Animated, View } from "react-native";
4 |
5 | import MentionListItem from "../MentionListItem";
6 | // Styles
7 | import styles from "./MentionListStyles";
8 |
9 | export class MentionList extends React.PureComponent {
10 | static propTypes = {
11 | list: PropTypes.array,
12 | editorStyles: PropTypes.object,
13 | isTrackingStarted: PropTypes.bool,
14 | suggestions: PropTypes.array,
15 | keyword: PropTypes.string,
16 | onSuggestionTap: PropTypes.func
17 | };
18 |
19 | constructor() {
20 | super();
21 | this.previousChar = " ";
22 | }
23 |
24 | renderSuggestionsRow = ({ item }) => {
25 | return (
26 |
31 | );
32 | };
33 | render() {
34 | const { props } = this;
35 |
36 | const { keyword, isTrackingStarted } = props;
37 | const withoutAtKeyword = keyword.substr(1, keyword.length);
38 | const list = this.props.list;
39 | const suggestions =
40 | withoutAtKeyword !== ""
41 | ? list.filter(user => user.username.includes(withoutAtKeyword))
42 | : list;
43 | if (!isTrackingStarted) {
44 | return null;
45 | }
46 | return (
47 |
53 |
59 |
60 |
61 | }
62 | enableEmptySections={true}
63 | data={suggestions}
64 | keyExtractor={(item, index) => `${item.id}-${index}`}
65 | renderItem={rowData => {
66 | return this.renderSuggestionsRow(rowData);
67 | }}
68 | />
69 |
70 | );
71 | }
72 | }
73 |
74 | export default MentionList;
75 |
--------------------------------------------------------------------------------
/src/MentionListItem/MentionListItemStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 |
3 | export default StyleSheet.create({
4 | suggestionItem: {
5 | flex: 1,
6 | flexDirection: "row",
7 | justifyContent: "flex-start",
8 | alignItems: "center",
9 | width: "100%",
10 | backgroundColor: "#fff",
11 | color: "rgba(0, 0, 0, 0.1)",
12 | height: 50,
13 | paddingHorizontal: 20,
14 | borderBottomWidth: 1,
15 | borderColor: "rgba(0, 0, 0, 0.05)"
16 | },
17 | text: {
18 | alignSelf: "center",
19 | marginLeft: 12
20 | },
21 | title: {
22 | fontSize: 16,
23 | color: "rgba(0, 0, 0, 0.8)"
24 | },
25 | thumbnailWrapper: {
26 | width: 35,
27 | height: 35
28 | },
29 | thumbnailChar: {
30 | fontSize: 16
31 | }
32 | });
33 |
--------------------------------------------------------------------------------
/src/MentionListItem/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Text, View, TouchableOpacity } from "react-native";
4 |
5 | // Styles
6 | import styles from "./MentionListItemStyles";
7 |
8 | import Avatar from "../Avatar";
9 |
10 | export class MentionListItem extends React.PureComponent {
11 | static propTypes = {
12 | item: PropTypes.object,
13 | onSuggestionTap: PropTypes.func,
14 | editorStyles: PropTypes.object
15 | };
16 |
17 | onSuggestionTap = (user, hidePanel) => {
18 | this.props.onSuggestionTap(user);
19 | };
20 |
21 | render() {
22 | const { item: user, index, editorStyles } = this.props;
23 | return (
24 |
25 | this.onSuggestionTap(user)}
29 | >
30 |
35 |
36 |
37 |
38 | {user.name}
39 |
40 |
43 | @{user.username}
44 |
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default MentionListItem;
53 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------