[RN] Add chat functionality
Co-authored-by: DimaG <dgeorgiev06@gmail.com>
This commit is contained in:
parent
82f714b608
commit
8a241ba2b7
|
@ -124,6 +124,7 @@ android.libraryVariants.all { def variant ->
|
|||
// Bundle sounds
|
||||
//
|
||||
copy {
|
||||
from("${projectDir}/../../sounds/incomingMessage.wav")
|
||||
from("${projectDir}/../../sounds/joined.wav")
|
||||
from("${projectDir}/../../sounds/left.wav")
|
||||
from("${projectDir}/../../sounds/outgoingRinging.wav")
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-chat-unread:before {
|
||||
content: "\e0b7";
|
||||
}
|
||||
.icon-arrow_back:before {
|
||||
content: "\e5c4";
|
||||
}
|
||||
|
|
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
|
@ -7,6 +7,7 @@
|
|||
<font-face units-per-em="1024" ascent="1024" descent="0" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " d="" />
|
||||
<glyph unicode="" glyph-name="chat-unread" d="M768 682v86h-512v-86h512zM598 426v86h-342v-86h342zM256 640v-86h512v86h-512zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
|
||||
<glyph unicode="" glyph-name="phone" d="M282 564c62-120 162-220 282-282l94 94c12 12 30 16 44 10 48-16 100-24 152-24 24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44z" />
|
||||
<glyph unicode="" glyph-name="add" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
|
||||
<glyph unicode="" glyph-name="bluetooth" d="M550 328l-80 82v-162zM470 776v-162l80 82zM670 696l-184-184 184-184-244-242h-42v324l-196-196-60 60 238 238-238 238 60 60 196-196v324h42zM834 738c40-64 62-142 62-222 0-84-24-160-66-226l-50 50c26 52 42 110 42 172s-16 120-42 172zM608 512l98 98c12-30 20-64 20-98s-8-70-20-100z" />
|
||||
|
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -32,6 +32,7 @@
|
|||
6C31EDCA20C06D530089C899 /* recordingOff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 6C31EDC920C06D530089C899 /* recordingOff.mp3 */; };
|
||||
75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; };
|
||||
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
|
||||
87FE6F3321E52437004A5DC7 /* incomingMessage.wav in Resources */ = {isa = PBXBuildFile; fileRef = 87FE6F3221E52437004A5DC7 /* incomingMessage.wav */; };
|
||||
A4414AE020B37F1A003546E6 /* rejected.wav in Resources */ = {isa = PBXBuildFile; fileRef = A4414ADF20B37F1A003546E6 /* rejected.wav */; };
|
||||
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */ = {isa = PBXBuildFile; fileRef = A4A934E8212F3ADB001E9388 /* Dropbox.m */; };
|
||||
B386B85720981A75000DEF7A /* InviteController.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85020981A74000DEF7A /* InviteController.m */; };
|
||||
|
@ -85,6 +86,7 @@
|
|||
6C31EDC920C06D530089C899 /* recordingOff.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = recordingOff.mp3; path = ../../sounds/recordingOff.mp3; sourceTree = "<group>"; };
|
||||
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
|
||||
75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
|
||||
87FE6F3221E52437004A5DC7 /* incomingMessage.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = incomingMessage.wav; path = ../../sounds/incomingMessage.wav; sourceTree = "<group>"; };
|
||||
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A4414ADF20B37F1A003546E6 /* rejected.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = rejected.wav; path = ../../sounds/rejected.wav; sourceTree = "<group>"; };
|
||||
|
@ -125,6 +127,7 @@
|
|||
0BCA49681EC4BBE500B793EE /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87FE6F3221E52437004A5DC7 /* incomingMessage.wav */,
|
||||
0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */,
|
||||
C6245F5B2053091D0040BE68 /* image-resize@2x.png */,
|
||||
C6245F5C2053091D0040BE68 /* image-resize@3x.png */,
|
||||
|
@ -335,6 +338,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
87FE6F3321E52437004A5DC7 /* incomingMessage.wav in Resources */,
|
||||
0B49424520AD8DBD00BD2DE0 /* outgoingStart.wav in Resources */,
|
||||
6C31EDCA20C06D530089C899 /* recordingOff.mp3 in Resources */,
|
||||
A4414AE020B37F1A003546E6 /* rejected.wav in Resources */,
|
||||
|
|
|
@ -53,8 +53,9 @@
|
|||
"messagebox": "Enter text...",
|
||||
"nickname": {
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname in the box below"
|
||||
}
|
||||
"title": "Enter a nickname to use chat"
|
||||
},
|
||||
"title": "Chat"
|
||||
},
|
||||
"connection": {
|
||||
"ATTACHED": "Attached",
|
||||
|
@ -642,7 +643,7 @@
|
|||
"Settings": "Settings",
|
||||
"sharedvideo": "Share a YouTube video",
|
||||
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
|
||||
"shareRoom": "Share room",
|
||||
"shareRoom": "Invite someone",
|
||||
"shortcuts": "View shortcuts",
|
||||
"sip": "Call SIP number",
|
||||
"speakerStats": "Speaker stats",
|
||||
|
|
|
@ -2166,6 +2166,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@expo/react-native-action-sheet": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@expo/react-native-action-sheet/-/react-native-action-sheet-1.1.2.tgz",
|
||||
"integrity": "sha512-//2EvHVBFVGSAzuJvG0I1UoQVzGJBo2f1CkO+RMnEWdR0FeWYmV7+pCThIroL1czRm/oOtoMxiGS6FgXt6QgVA==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^2.2.2",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"@jitsi/sdp-interop": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-0.1.13.tgz",
|
||||
|
@ -2193,7 +2202,7 @@
|
|||
},
|
||||
"@segment/top-domain": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "http://registry.npmjs.org/@segment/top-domain/-/top-domain-3.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/@segment/top-domain/-/top-domain-3.0.0.tgz",
|
||||
"integrity": "sha1-AuWlpP1CqfbPiGsF6C8QQBKjw6c=",
|
||||
"requires": {
|
||||
"component-cookie": "^1.1.2",
|
||||
|
@ -3142,6 +3151,14 @@
|
|||
"util.promisify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-check-es2015-constants": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
|
||||
"integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.22.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-syntax-trailing-function-commas": {
|
||||
"version": "7.0.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz",
|
||||
|
@ -4112,7 +4129,7 @@
|
|||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
|
||||
"requires": {
|
||||
"ms": "0.7.1"
|
||||
|
@ -4120,7 +4137,7 @@
|
|||
},
|
||||
"ms": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
|
||||
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
|
||||
}
|
||||
}
|
||||
|
@ -8368,6 +8385,11 @@
|
|||
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.1.9.tgz",
|
||||
"integrity": "sha1-lkojxU5IiUBbSGGlyfBIDUUUHfo="
|
||||
},
|
||||
"keymirror": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
|
||||
"integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
@ -11444,11 +11466,37 @@
|
|||
"jssha": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"react-native-communications": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-communications/-/react-native-communications-2.2.1.tgz",
|
||||
"integrity": "sha1-eIO1ayCgAu63kMET+GFuqGksp5U="
|
||||
},
|
||||
"react-native-fast-image": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.1.1.tgz",
|
||||
"integrity": "sha512-kEzgZxbbXYhy27u5GnhrKitn+XDBFAHSDUJdYC6llMi5cDPjgcqhOAQABj0K+ga5pn+/xPZLmD882rrUGiwVVA=="
|
||||
},
|
||||
"react-native-gifted-chat": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gifted-chat/-/react-native-gifted-chat-0.6.0.tgz",
|
||||
"integrity": "sha512-KYI/okKUZmjcJM3I6BP10KG1WNkCKBZhY8N47wk407dr+KqLS4+LR13UKo7j3f++5SrX2Ex+7vYvIQ2pBdzCiA==",
|
||||
"requires": {
|
||||
"@expo/react-native-action-sheet": "^1.0.1",
|
||||
"moment": "^2.19.0",
|
||||
"react-native-communications": "2.2.1",
|
||||
"react-native-lightbox": "^0.7.0",
|
||||
"react-native-parsed-text": "^0.0.20",
|
||||
"react-native-video": "^3.2.1",
|
||||
"uuid": "3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.0.tgz",
|
||||
"integrity": "sha512-ijO9N2xY/YaOqQ5yz5c4sy2ZjWmA6AR6zASb/gdpeKZ8+948CxwfMW9RrKVk5may6ev8c0/Xguu32e2Llelpqw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-google-signin": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.2.tgz",
|
||||
|
@ -11464,11 +11512,28 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-keep-awake/-/react-native-keep-awake-4.0.0.tgz",
|
||||
"integrity": "sha512-0Fotox+eLXQooeibVs3P60yASYUWjtRw9MZNmbuHt5UZQrgUrAKsE4jm7gTr4tPU1m1RkwGzcgUFpcOkh/ec7g=="
|
||||
},
|
||||
"react-native-lightbox": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz",
|
||||
"integrity": "sha512-HS3T4WlCd0Gb3us2d6Jse5m6KjNhngnKm35Wapq30WtQa9s+/VMmtuktbGPGaWtswcDyOj6qByeJBw9W80iPCA==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-native-linear-gradient": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.5.3.tgz",
|
||||
"integrity": "sha512-XdusrOXXlkI+yQpUW7YLeiq9cZiBwkvQX4XEkHPVrJ9H47gsKmdgBwObkZBzBQUP0dKK/Sg6aVpETEis4w43bQ=="
|
||||
},
|
||||
"react-native-parsed-text": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/react-native-parsed-text/-/react-native-parsed-text-0.0.20.tgz",
|
||||
"integrity": "sha512-n77hYu64Tr3oclzIXBXXaiLh1WbMKdA2Y0x6bX/yqwxAM4afcObENY5VrNB+EsTBJBEDqrypA9D1p2cLEIHkuQ==",
|
||||
"requires": {
|
||||
"babel-plugin-check-es2015-constants": "6.22.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-native-sound": {
|
||||
"version": "github:jitsi/react-native-sound#e4260ed7f641eeb0377d76eac7987aba72e1cf08",
|
||||
"from": "github:jitsi/react-native-sound#e4260ed7f641eeb0377d76eac7987aba72e1cf08"
|
||||
|
@ -11524,6 +11589,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-native-video": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-3.2.1.tgz",
|
||||
"integrity": "sha512-Xansfoo/to80FwhM1HKlf7pCxDZ5RtV+kG3piCVvsNAhPY4GGwiOGUH9y3Y+mFQIDEWcY8I9j16lsFYAbnue3g==",
|
||||
"requires": {
|
||||
"keymirror": "0.1.1",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "github:jitsi/react-native-webrtc#c1be0cb1c6e8a83dfd406e478082a5ff205a97ec",
|
||||
"from": "github:jitsi/react-native-webrtc#c1be0cb1c6e8a83dfd406e478082a5ff205a97ec",
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
"react-native-calendar-events": "1.6.4",
|
||||
"react-native-callstats": "3.53.4",
|
||||
"react-native-fast-image": "5.1.1",
|
||||
"react-native-gifted-chat": "0.6.0",
|
||||
"react-native-google-signin": "1.0.2",
|
||||
"react-native-immersive": "2.0.0",
|
||||
"react-native-keep-awake": "4.0.0",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* The type of the action which signals to add a new chat message.
|
||||
*
|
||||
|
@ -14,7 +16,7 @@
|
|||
export const ADD_MESSAGE = Symbol('ADD_MESSAGE');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to remove all saved chat messages.
|
||||
* The type of the action which signals to clear messages in Redux.
|
||||
*
|
||||
* {
|
||||
* type: CLEAR_MESSAGES
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
|
@ -27,7 +29,7 @@ import {
|
|||
* timestamp: string,
|
||||
* }}
|
||||
*/
|
||||
export function addMessage(messageDetails) {
|
||||
export function addMessage(messageDetails: Object) {
|
||||
return {
|
||||
type: ADD_MESSAGE,
|
||||
...messageDetails
|
||||
|
@ -35,7 +37,7 @@ export function addMessage(messageDetails) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Removes all stored chat messages.
|
||||
* Clears the chat messages in Redux.
|
||||
*
|
||||
* @returns {{
|
||||
* type: CLEAR_MESSAGES
|
||||
|
@ -56,7 +58,7 @@ export function clearMessages() {
|
|||
* message: string
|
||||
* }}
|
||||
*/
|
||||
export function sendMessage(message) {
|
||||
export function sendMessage(message: string) {
|
||||
return {
|
||||
type: SEND_MESSAGE,
|
||||
message
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
// @flow
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
|
||||
import { sendMessage, toggleChat } from '../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@code AbstractChat}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* True if the chat window should be rendered.
|
||||
*/
|
||||
_isOpen: boolean,
|
||||
|
||||
/**
|
||||
* All the chat messages in the conference.
|
||||
*/
|
||||
_messages: Array<Object>,
|
||||
|
||||
/**
|
||||
* Function to send a text message.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_onSendMessage: Function,
|
||||
|
||||
/**
|
||||
* Function to toggle the chat window.
|
||||
*/
|
||||
_onToggleChat: Function,
|
||||
|
||||
/**
|
||||
* Whether or not to block chat access with a nickname input form.
|
||||
*/
|
||||
_showNamePrompt: boolean,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Dispatch<*>,
|
||||
|
||||
/**
|
||||
* Function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an abstract chat panel.
|
||||
*/
|
||||
export default class AbstractChat<P: Props> extends Component<P> {}
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* _onSendMessage: Function,
|
||||
* _onToggleChat: Function
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: Dispatch<*>) {
|
||||
return {
|
||||
/**
|
||||
* Toggles the chat window.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onToggleChat() {
|
||||
dispatch(toggleChat());
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
*
|
||||
* @private
|
||||
* @param {string} text - The text message to be sent.
|
||||
* @returns {void}
|
||||
* @type {Function}
|
||||
*/
|
||||
_onSendMessage(text: string) {
|
||||
dispatch(sendMessage(text));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to {@link Chat} React {@code Component}
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux store/state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isOpen: boolean,
|
||||
* _messages: Array<Object>,
|
||||
* _showNamePrompt: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
const { isOpen, messages } = state['features/chat'];
|
||||
const _localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_isOpen: isOpen,
|
||||
_messages: messages,
|
||||
_showNamePrompt: !_localParticipant.name
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// @flow
|
||||
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { getAvatarURLByParticipantId } from '../../base/participants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@code AbstractChatMessage}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The URL of the avatar of the participant.
|
||||
*/
|
||||
_avatarURL: string,
|
||||
|
||||
/**
|
||||
* The representation of a chat message.
|
||||
*/
|
||||
message: Object,
|
||||
|
||||
/**
|
||||
* Invoked to receive translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract component to display a chat message.
|
||||
*/
|
||||
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {{
|
||||
* _avatarURL: string
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { message } = ownProps;
|
||||
|
||||
return {
|
||||
_avatarURL: getAvatarURLByParticipantId(state, message.user._id)
|
||||
};
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export Chat from './Chat';
|
||||
export ChatCounter from './ChatCounter';
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './native';
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
export * from './web';
|
|
@ -0,0 +1,168 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { GiftedChat } from 'react-native-gifted-chat';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { BackButton, Header, HeaderLabel, Modal } from '../../../base/react';
|
||||
|
||||
import AbstractChat, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps as _abstractMapStateToProps,
|
||||
type Props as AbstractProps
|
||||
} from '../AbstractChat';
|
||||
|
||||
import ChatMessage from './ChatMessage';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* True if the chat window should have a solid BG render.
|
||||
*/
|
||||
_solidBackground: boolean
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements a React native component that renders the chat window (modal) of
|
||||
* the mobile client.
|
||||
*/
|
||||
class Chat extends AbstractChat<Props> {
|
||||
|
||||
/**
|
||||
* Initializes a new instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onSend = this._onSend.bind(this);
|
||||
this._renderMessage = this._renderMessage.bind(this);
|
||||
this._transformMessage = this._transformMessage.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const messages = this.props._messages.map(this._transformMessage);
|
||||
const modalStyle = [
|
||||
styles.modalBackdrop
|
||||
];
|
||||
|
||||
if (this.props._solidBackground) {
|
||||
// We only use a transparent background, when we are in a video
|
||||
// meeting to give a user a glympse of what's happening. Otherwise
|
||||
// we use a non-transparent background.
|
||||
modalStyle.push(styles.solidModalBackdrop);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onRequestClose = { this.props._onToggleChat }
|
||||
visible = { this.props._isOpen }>
|
||||
<Header>
|
||||
<BackButton onPress = { this.props._onToggleChat } />
|
||||
<HeaderLabel labelKey = 'chat.title' />
|
||||
</Header>
|
||||
<SafeAreaView style = { modalStyle }>
|
||||
<GiftedChat
|
||||
messages = { messages }
|
||||
onSend = { this._onSend }
|
||||
renderMessage = { this._renderMessage } />
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
_onSend: (Array<Object>) => void;
|
||||
|
||||
/**
|
||||
* Callback to trigger a message send action.
|
||||
*
|
||||
* @param {string} message - The chat message to display.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSend([ message ]) {
|
||||
this.props._onSendMessage(message.text);
|
||||
}
|
||||
|
||||
_renderMessage: Object => React$Element<*>
|
||||
|
||||
/**
|
||||
* Renders a single message.
|
||||
*
|
||||
* @param {Object} messageProps - The message props object to be rendered.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderMessage(messageProps) {
|
||||
const { currentMessage } = messageProps;
|
||||
|
||||
return (
|
||||
<ChatMessage message = { currentMessage } />
|
||||
);
|
||||
}
|
||||
|
||||
_transformMessage: (Object, number) => Object;
|
||||
|
||||
/**
|
||||
* Transforms a Jitsi message object to a format that gifted-chat can
|
||||
* handle.
|
||||
*
|
||||
* @param {Object} message - The chat message in our internal format.
|
||||
* @param {number} index - The index of the message in the array.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_transformMessage(message, index) {
|
||||
const system = message.messageType === 'error';
|
||||
|
||||
return (
|
||||
{
|
||||
_id: index,
|
||||
createdAt: new Date(message.timestamp),
|
||||
messageType: message.messageType,
|
||||
system,
|
||||
text: system
|
||||
? this.props.t('chat.error', {
|
||||
error: message.error,
|
||||
originalText: message.message
|
||||
})
|
||||
: message.message,
|
||||
user: {
|
||||
_id: message.id,
|
||||
name: message.displayName
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {{
|
||||
* _solidBackground: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const abstractReduxProps = _abstractMapStateToProps(state);
|
||||
|
||||
return {
|
||||
...abstractReduxProps,
|
||||
|
||||
// Gifted chat requires the messages to be reverse ordered.
|
||||
_messages: [
|
||||
...abstractReduxProps._messages
|
||||
].reverse(),
|
||||
_solidBackground: state['features/base/conference'].audioOnly
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));
|
|
@ -0,0 +1,129 @@
|
|||
// @flow
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import {
|
||||
AbstractButton,
|
||||
type AbstractButtonProps
|
||||
} from '../../../base/toolbox';
|
||||
import { openDisplayNamePrompt } from '../../../display-name';
|
||||
|
||||
import { toggleChat } from '../../actions';
|
||||
import { getUnreadCount } from '../../functions';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Function to display chat.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_displayChat: Function,
|
||||
|
||||
/**
|
||||
* Function to diaply the name prompt before displaying the chat
|
||||
* window, if the user has no display name set.
|
||||
*/
|
||||
_displayNameInputDialog: Function,
|
||||
|
||||
/**
|
||||
* Whether or not to block chat access with a nickname input form.
|
||||
*/
|
||||
_showNamePrompt: boolean,
|
||||
|
||||
/**
|
||||
* The unread message count.
|
||||
*/
|
||||
_unreadMessageCount: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an {@link AbstractButton} to open the chat screen on mobile.
|
||||
*/
|
||||
class ChatButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.chat';
|
||||
iconName = 'chat';
|
||||
label = 'toolbar.chat';
|
||||
toggledIconName = 'chat-unread';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
if (this.props._showNamePrompt) {
|
||||
this.props._displayNameInputDialog(() => {
|
||||
this.props._displayChat();
|
||||
});
|
||||
} else {
|
||||
this.props._displayChat();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button toggled when there are unread messages.
|
||||
*
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return Boolean(this.props._unreadMessageCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* _displayChat,
|
||||
* _displayNameInputDialog
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch: Function) {
|
||||
return {
|
||||
/**
|
||||
* Launches native invite dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayChat() {
|
||||
dispatch(toggleChat());
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a diaply name prompt.
|
||||
*
|
||||
* @param {Function} onPostSubmit - The function to invoke after a
|
||||
* succesfulsetting of the display name.
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayNameInputDialog(onPostSubmit) {
|
||||
dispatch(openDisplayNamePrompt(onPostSubmit));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the redux state to the component's props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {{
|
||||
* _unreadMessageCount
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_showNamePrompt: !localParticipant.name,
|
||||
_unreadMessageCount: getUnreadCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(ChatButton);
|
|
@ -0,0 +1,152 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getLocalizedDateFormatter, translate } from '../../../base/i18n';
|
||||
import { Avatar } from '../../../base/participants';
|
||||
|
||||
import AbstractChatMessage, {
|
||||
_mapStateToProps as _abstractMapStateToProps,
|
||||
type Props as AbstractProps
|
||||
} from '../AbstractChatMessage';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Size of the rendered avatar in the message.
|
||||
*/
|
||||
const AVATAR_SIZE = 32;
|
||||
|
||||
/**
|
||||
* Formatter string to display the message timestamp.
|
||||
*/
|
||||
const TIMESTAMP_FORMAT = 'H:mm';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* True if the chat window has a solid BG so then we have to adopt in style.
|
||||
*/
|
||||
_solidBackground: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single chat message.
|
||||
*/
|
||||
class ChatMessage extends AbstractChatMessage<Props> {
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
const timeStamp = getLocalizedDateFormatter(
|
||||
message.createdAt).format(TIMESTAMP_FORMAT);
|
||||
const localMessage = message.messageType === 'local';
|
||||
|
||||
// Style arrays that need to be updated in various scenarios, such as
|
||||
// error messages or others.
|
||||
const detailsWrapperStyle = [
|
||||
styles.detailsWrapper
|
||||
];
|
||||
const textWrapperStyle = [
|
||||
styles.textWrapper
|
||||
];
|
||||
const timeTextStyles = [
|
||||
styles.timeText
|
||||
];
|
||||
|
||||
if (localMessage) {
|
||||
// The wrapper needs to be aligned to the right.
|
||||
detailsWrapperStyle.push(styles.ownMessageDetailsWrapper);
|
||||
|
||||
// The bubble needs to be differently styled.
|
||||
textWrapperStyle.push(styles.ownTextWrapper);
|
||||
} else if (message.system) {
|
||||
// The bubble needs to be differently styled.
|
||||
textWrapperStyle.push(styles.systemTextWrapper);
|
||||
}
|
||||
|
||||
if (this.props._solidBackground) {
|
||||
timeTextStyles.push(styles.solidBGTimeText);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.messageWrapper } >
|
||||
{
|
||||
|
||||
// Avatar is only rendered for remote messages.
|
||||
!localMessage && this._renderAvatar()
|
||||
}
|
||||
<View style = { detailsWrapperStyle }>
|
||||
<View style = { textWrapperStyle } >
|
||||
{
|
||||
|
||||
// Display name is only rendered for remote
|
||||
// messages.
|
||||
!localMessage && this._renderDisplayName()
|
||||
}
|
||||
<Text style = { styles.messageText }>
|
||||
{ message.text }
|
||||
</Text>
|
||||
</View>
|
||||
<Text style = { timeTextStyles }>
|
||||
{ timeStamp }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the avatar of the sender.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderAvatar() {
|
||||
const { _avatarURL } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.avatarWrapper }>
|
||||
<Avatar
|
||||
size = { AVATAR_SIZE }
|
||||
uri = { _avatarURL } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the display name of the sender.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderDisplayName() {
|
||||
const { message } = this.props;
|
||||
|
||||
return (
|
||||
<Text style = { styles.displayName }>
|
||||
{ message.user.name }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {{
|
||||
* _solidBackground: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state, ownProps),
|
||||
_solidBackground: state['features/base/conference'].audioOnly
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(ChatMessage));
|
|
@ -0,0 +1,4 @@
|
|||
// @flow
|
||||
|
||||
export { default as Chat } from './Chat';
|
||||
export { default as ChatButton } from './ChatButton';
|
|
@ -0,0 +1,124 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
ColorPalette,
|
||||
createStyleSheet
|
||||
} from '../../../base/styles';
|
||||
|
||||
/**
|
||||
* The styles of the feature chat.
|
||||
*
|
||||
* NOTE: Sizes and colors come from the 8x8 guidelines. This is the first
|
||||
* component to receive this treating, if others happen to have similar, we
|
||||
* need to extract the brand colors and sizes into a branding feature (planned
|
||||
* for the future).
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Wrapper View for the avatar.
|
||||
*/
|
||||
avatarWrapper: {
|
||||
marginRight: 8
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for the details together, such as name, message and time.
|
||||
*/
|
||||
detailsWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
* The text node for the display name.
|
||||
*/
|
||||
displayName: {
|
||||
color: 'rgb(118, 136, 152)',
|
||||
fontSize: 13
|
||||
},
|
||||
|
||||
/**
|
||||
* The message text itself.
|
||||
*/
|
||||
messageText: {
|
||||
color: 'rgb(28, 32, 37)',
|
||||
fontSize: 15
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper View for the entire block.
|
||||
*/
|
||||
messageWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 17,
|
||||
marginVertical: 4
|
||||
},
|
||||
|
||||
/**
|
||||
* Background of the chat screen. Currently it's set to a transparent value
|
||||
* as the idea is that the participant would still want to see at least a
|
||||
* part of the video when he/she is in the chat window.
|
||||
*/
|
||||
modalBackdrop: {
|
||||
backgroundColor: 'rgba(127, 127, 127, 0.8)',
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Style modifier for the {@code detailsWrapper} for own messages.
|
||||
*/
|
||||
ownMessageDetailsWrapper: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style modifier for the {@code textWrapper} for own messages.
|
||||
*/
|
||||
ownTextWrapper: {
|
||||
backgroundColor: 'rgb(210, 231, 249)',
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 0
|
||||
},
|
||||
|
||||
solidBGTimeText: {
|
||||
color: 'rgb(164, 184, 209)'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style modifier for the chat window when we're in audio only mode.
|
||||
*/
|
||||
solidModalBackdrop: {
|
||||
backgroundColor: ColorPalette.white
|
||||
},
|
||||
|
||||
/**
|
||||
* Style modifier for system (error) messages.
|
||||
*/
|
||||
systemTextWrapper: {
|
||||
backgroundColor: 'rgb(247, 215, 215)'
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for the name and the message text.
|
||||
*/
|
||||
textWrapper: {
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: 'rgb(240, 243, 247)',
|
||||
borderRadius: 8,
|
||||
borderTopLeftRadius: 0,
|
||||
flexDirection: 'column',
|
||||
padding: 9
|
||||
},
|
||||
|
||||
/**
|
||||
* Text node for the timestamp.
|
||||
*/
|
||||
timeText: {
|
||||
color: ColorPalette.white,
|
||||
fontSize: 13
|
||||
}
|
||||
});
|
|
@ -1,79 +1,25 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
|
||||
import { toggleChat } from '../actions';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import AbstractChat, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractChat';
|
||||
import ChatInput from './ChatInput';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import DisplayNameForm from './DisplayNameForm';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Chat}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The JitsiConference instance to send messages to.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* Whether or not chat is displayed.
|
||||
*/
|
||||
_isOpen: Boolean,
|
||||
|
||||
/**
|
||||
* The local participant's ID.
|
||||
*/
|
||||
_localUserId: String,
|
||||
|
||||
/**
|
||||
* All the chat messages in the conference.
|
||||
*/
|
||||
_messages: Array<Object>,
|
||||
|
||||
/**
|
||||
* Whether or not to block chat access with a nickname input form.
|
||||
*/
|
||||
_showNamePrompt: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to change the chat panel status.
|
||||
*/
|
||||
dispatch: Dispatch<*>
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@Chat}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* User provided nickname when the input text is provided in the view.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
message: string
|
||||
};
|
||||
|
||||
/**
|
||||
* React Component for holding the chat feature in a side panel that slides in
|
||||
* and out of view.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Chat extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Reference to the HTML element used for typing in a chat message.
|
||||
*/
|
||||
_chatInput: ?HTMLElement;
|
||||
class Chat extends AbstractChat<Props> {
|
||||
|
||||
/**
|
||||
* Whether or not the {@code Chat} component is off-screen, having finished
|
||||
|
@ -96,15 +42,12 @@ class Chat extends Component<Props, State> {
|
|||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._chatInput = null;
|
||||
this._isExited = true;
|
||||
this._messagesListEnd = null;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onCloseClick = this._onCloseClick.bind(this);
|
||||
this._renderMessage = this._renderMessage.bind(this);
|
||||
this._renderPanelContent = this._renderPanelContent.bind(this);
|
||||
this._setChatInputRef = this._setChatInputRef.bind(this);
|
||||
this._setMessageListEndRef = this._setMessageListEndRef.bind(this);
|
||||
}
|
||||
|
||||
|
@ -145,17 +88,6 @@ class Chat extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
_onCloseClick: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to hide {@code Chat}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseClick() {
|
||||
this.props.dispatch(toggleChat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React Element for showing chat messages and a form to send new
|
||||
* chat messages.
|
||||
|
@ -177,7 +109,7 @@ class Chat extends Component<Props, State> {
|
|||
<div id = 'chatconversation'>
|
||||
{ messages }
|
||||
</div>
|
||||
<ChatInput getChatInputRef = { this._setChatInputRef } />
|
||||
<ChatInput />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -213,14 +145,14 @@ class Chat extends Component<Props, State> {
|
|||
_renderPanelContent(state) {
|
||||
this._isExited = state === 'exited';
|
||||
|
||||
const { _isOpen, _showNamePrompt } = this.props;
|
||||
const { _isOpen, _onToggleChat, _showNamePrompt } = this.props;
|
||||
const ComponentToRender = !_isOpen && state === 'exited'
|
||||
? null
|
||||
: (
|
||||
<div>
|
||||
<div
|
||||
className = 'chat-close'
|
||||
onClick = { this._onCloseClick }>X</div>
|
||||
onClick = { _onToggleChat }>X</div>
|
||||
{ _showNamePrompt
|
||||
? <DisplayNameForm /> : this._renderChat() }
|
||||
</div>
|
||||
|
@ -256,20 +188,6 @@ class Chat extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
_setChatInputRef: (?HTMLElement) => void;
|
||||
|
||||
/**
|
||||
* Sets a reference to the HTML text input element used for typing in chat
|
||||
* messages.
|
||||
*
|
||||
* @param {Object} chatInput - The input for typing chat messages.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setChatInputRef(chatInput: ?HTMLElement) {
|
||||
this._chatInput = chatInput;
|
||||
}
|
||||
|
||||
_setMessageListEndRef: (?HTMLElement) => void;
|
||||
|
||||
/**
|
||||
|
@ -284,29 +202,4 @@ class Chat extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to {@link Chat} React {@code Component}
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux store/state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: Object,
|
||||
* _isOpen: boolean,
|
||||
* _messages: Array<Object>,
|
||||
* _showNamePrompt: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { isOpen, messages } = state['features/chat'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_isOpen: isOpen,
|
||||
_messages: messages,
|
||||
_showNamePrompt: !localParticipant.name
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(Chat));
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getUnreadCount } from '../functions';
|
||||
import { getUnreadCount } from '../../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ChatCounter}.
|
|
@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import Emoji from 'react-emoji-render';
|
||||
|
||||
import { sendMessage } from '../actions';
|
||||
import { sendMessage } from '../../actions';
|
||||
|
||||
import SmileysPanel from './SmileysPanel';
|
||||
|
|
@ -1,34 +1,20 @@
|
|||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { toArray } from 'react-emoji-render';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import AbstractChatMessage, {
|
||||
type Props
|
||||
} from '../AbstractChatMessage';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Chat}.
|
||||
* Renders a single chat message.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The redux representation of a chat message.
|
||||
*/
|
||||
message: Object,
|
||||
|
||||
/**
|
||||
* Invoked to receive translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays as passed in chat message.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class ChatMessage extends PureComponent<Props> {
|
||||
class ChatMessage extends AbstractChatMessage<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
|
@ -4,8 +4,8 @@ import { FieldTextStateless } from '@atlaskit/field-text';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { updateSettings } from '../../base/settings';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { updateSettings } from '../../../base/settings';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@DisplayNameForm}.
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React, { PureComponent } from 'react';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { smileys } from '../smileys';
|
||||
import { smileys } from '../../smileys';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SmileysPanel}.
|
|
@ -0,0 +1,4 @@
|
|||
// @flow
|
||||
|
||||
export { default as Chat } from './Chat';
|
||||
export { default as ChatCounter } from './ChatCounter';
|
|
@ -3,15 +3,21 @@
|
|||
import UIUtil from '../../../modules/UI/util/UIUtil';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../base/conference';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
getCurrentConference
|
||||
} from '../base/conference';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getParticipantById } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import {
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { isButtonEnabled, showToolbox } from '../toolbox';
|
||||
|
||||
import { SEND_MESSAGE } from './actionTypes';
|
||||
import { addMessage, clearMessages } from './actions';
|
||||
import { addMessage, clearMessages, toggleChat } from './actions';
|
||||
import { INCOMING_MSG_SOUND_ID } from './constants';
|
||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||
|
||||
|
@ -27,49 +33,60 @@ declare var interfaceConfig : Object;
|
|||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
// Register the chat message sound on Web only because there's no chat
|
||||
// on mobile.
|
||||
typeof APP === 'undefined'
|
||||
|| store.dispatch(
|
||||
store.dispatch(
|
||||
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
// Unregister the chat message sound on Web because it's registered
|
||||
// there only.
|
||||
typeof APP === 'undefined'
|
||||
|| store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
||||
store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
typeof APP === 'undefined'
|
||||
|| _addChatMsgListener(action.conference, store);
|
||||
_addChatMsgListener(action.conference, store);
|
||||
break;
|
||||
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
store.dispatch(clearMessages());
|
||||
break;
|
||||
case SEND_MESSAGE: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
case SEND_MESSAGE:
|
||||
if (typeof APP !== 'undefined') {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
const escapedMessage = UIUtil.escapeHtml(action.message);
|
||||
if (conference) {
|
||||
const escapedMessage = UIUtil.escapeHtml(action.message);
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifySendingChatMessage(escapedMessage);
|
||||
conference.sendTextMessage(escapedMessage);
|
||||
}
|
||||
conference.sendTextMessage(escapedMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers listener for {@link JitsiConferenceEvents.MESSAGE_RECEIVED} which
|
||||
* will play a sound on the event, given that the chat is not currently visible.
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, e.g. clear messages or close the chat modal if it's left
|
||||
* open.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch, getState }, previousConference) => {
|
||||
if (conference !== previousConference) {
|
||||
// conference changed, left or failed...
|
||||
|
||||
if (getState()['features/chat'].isOpen) {
|
||||
// Closes the chat if it's left open.
|
||||
dispatch(toggleChat());
|
||||
}
|
||||
|
||||
// Clear chat messages.
|
||||
dispatch(clearMessages());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers listener for {@link JitsiConferenceEvents.MESSAGE_RECEIVED} that
|
||||
* will perform various chat related activities.
|
||||
*
|
||||
* @param {JitsiConference} conference - The conference instance on which the
|
||||
* new event listener will be registered.
|
||||
|
@ -79,35 +96,28 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
*/
|
||||
function _addChatMsgListener(conference, { dispatch, getState }) {
|
||||
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|
||||
|| !isButtonEnabled('chat')) {
|
||||
|| (typeof APP !== 'undefined' && !isButtonEnabled('chat'))) {
|
||||
// We don't register anything on web if we're in filmStripOnly mode, or
|
||||
// the chat button is not enabled in interfaceConfig.
|
||||
return;
|
||||
}
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
(id, message, timestamp) => {
|
||||
// Logic for all platforms:
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
|
||||
if (!isChatOpen) {
|
||||
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
dispatch(showToolbox(4000));
|
||||
}
|
||||
|
||||
// Provide a default for for the case when a message is being
|
||||
// backfilled for a participant that has left the conference.
|
||||
const participant = getParticipantById(state, id) || {};
|
||||
const displayName = participant.name
|
||||
|| `${interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME} (${id})`;
|
||||
const displayName = getParticipantDisplayName(getState, id);
|
||||
const hasRead = participant.local || isChatOpen;
|
||||
|
||||
APP.API.notifyReceivedChatMessage({
|
||||
body: message,
|
||||
id,
|
||||
nick: displayName,
|
||||
ts: timestamp
|
||||
});
|
||||
|
||||
const timestampToDate = timestamp
|
||||
? new Date(timestamp) : new Date();
|
||||
const millisecondsTimestamp = timestampToDate.getTime();
|
||||
|
@ -120,6 +130,19 @@ function _addChatMsgListener(conference, { dispatch, getState }) {
|
|||
message,
|
||||
timestamp: millisecondsTimestamp
|
||||
}));
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
// Logic for web only:
|
||||
|
||||
APP.API.notifyReceivedChatMessage({
|
||||
body: message,
|
||||
id,
|
||||
nick: displayName,
|
||||
ts: timestamp
|
||||
});
|
||||
|
||||
dispatch(showToolbox(4000));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { TestConnectionInfo } from '../../base/testing';
|
||||
import { createDesiredLocalTracks } from '../../base/tracks';
|
||||
import { ConferenceNotification } from '../../calendar-sync';
|
||||
import { Chat } from '../../chat';
|
||||
import {
|
||||
Filmstrip,
|
||||
isFilmstripVisible,
|
||||
|
@ -250,6 +251,8 @@ class Conference extends Component<Props> {
|
|||
hidden = { true }
|
||||
translucent = { true } />
|
||||
|
||||
<Chat />
|
||||
|
||||
{/*
|
||||
* The LargeVideo is the lowermost stacking layer.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { InputDialog } from '../../base/dialog';
|
||||
|
||||
import AbstractDisplayNamePrompt from './AbstractDisplayNamePrompt';
|
||||
|
||||
/**
|
||||
* Implements a component to render a display name prompt.
|
||||
*/
|
||||
class DisplayNamePrompt extends AbstractDisplayNamePrompt<*> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<InputDialog
|
||||
contentKey = 'dialog.enterDisplayName'
|
||||
onSubmit = { this._onSetDisplayName } />
|
||||
);
|
||||
}
|
||||
|
||||
_onSetDisplayName: string => boolean;
|
||||
}
|
||||
|
||||
export default connect()(DisplayNamePrompt);
|
|
@ -9,6 +9,7 @@ import {
|
|||
bottomSheetItemStylesCombined,
|
||||
hideDialog
|
||||
} from '../../../base/dialog';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||
|
@ -86,6 +87,7 @@ class OverflowMenu extends Component<Props> {
|
|||
}
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<TileViewButton { ...buttonProps } />
|
||||
<InviteButton { ...buttonProps } />
|
||||
<PictureInPictureButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
|
|
|
@ -5,12 +5,13 @@ import { View } from 'react-native';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { Container } from '../../../base/react';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { ChatButton } from '../../../chat';
|
||||
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
import HangupButton from '../HangupButton';
|
||||
import OverflowMenuButton from './OverflowMenuButton';
|
||||
import styles, {
|
||||
chatButtonOverride,
|
||||
hangupButtonStyles,
|
||||
toolbarButtonStyles,
|
||||
toolbarToggledButtonStyles
|
||||
|
@ -141,6 +142,35 @@ class Toolbox extends Component<Props, State> {
|
|||
return 2 * Math.round(buttonSize / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the toggled style of the chat button. This cannot be done by
|
||||
* simple style inheritance due to the size calculation done in this
|
||||
* component.
|
||||
*
|
||||
* @param {Object} baseStyle - The base style that was originally
|
||||
* calculated.
|
||||
* @returns {Object | Array}
|
||||
*/
|
||||
_getChatButtonToggledStyle(baseStyle) {
|
||||
if (Array.isArray(baseStyle.style)) {
|
||||
return {
|
||||
...baseStyle,
|
||||
style: [
|
||||
...baseStyle.style,
|
||||
chatButtonOverride.toggled
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseStyle,
|
||||
style: [
|
||||
baseStyle.style,
|
||||
chatButtonOverride.toggled
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
_onLayout: (Object) => void;
|
||||
|
||||
/**
|
||||
|
@ -200,7 +230,11 @@ class Toolbox extends Component<Props, State> {
|
|||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.toolbar }>
|
||||
<InviteButton styles = { buttonStyles } />
|
||||
<ChatButton
|
||||
styles = { buttonStyles }
|
||||
toggledStyles = {
|
||||
this._getChatButtonToggledStyle(toggledButtonStyles)
|
||||
} />
|
||||
<AudioMuteButton
|
||||
styles = { buttonStyles }
|
||||
toggledStyles = { toggledButtonStyles } />
|
||||
|
|
|
@ -130,3 +130,13 @@ export const toolbarToggledButtonStyles = {
|
|||
iconStyle: styles.whiteToolbarButtonIcon,
|
||||
style: styles.whiteToolbarButton
|
||||
};
|
||||
|
||||
/**
|
||||
* Overrides to the standard styles that we apply to the chat button, as that
|
||||
* behaves slightly differently to other buttons.
|
||||
*/
|
||||
export const chatButtonOverride = createStyleSheet({
|
||||
toggled: {
|
||||
backgroundColor: ColorPalette.blue
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue