From 8a241ba2b73d7eb57a60e745b69c877ccf1508a5 Mon Sep 17 00:00:00 2001 From: Bettenbuk Zoltan Date: Sun, 13 Jan 2019 20:34:38 +0100 Subject: [PATCH] [RN] Add chat functionality Co-authored-by: DimaG --- android/sdk/build.gradle | 1 + css/_font.scss | 3 + fonts/jitsi.eot | Bin 12732 -> 12840 bytes fonts/jitsi.svg | 1 + fonts/jitsi.ttf | Bin 12576 -> 12684 bytes fonts/jitsi.woff | Bin 12652 -> 12760 bytes fonts/selection.json | 2 +- ios/sdk/sdk.xcodeproj/project.pbxproj | 4 + lang/main.json | 7 +- package-lock.json | 80 ++++++++- package.json | 1 + react/features/chat/actionTypes.js | 4 +- react/features/chat/actions.js | 8 +- .../features/chat/components/AbstractChat.js | 113 ++++++++++++ .../chat/components/AbstractChatMessage.js | 48 +++++ react/features/chat/components/Chat.native.js | 0 .../chat/components/ChatCounter.native.js | 0 .../chat/components/ChatInput.native.js | 0 .../chat/components/ChatMessage.native.js | 0 .../chat/components/DisplayNameForm.native.js | 0 .../chat/components/SmileysPanel.native.js | 0 react/features/chat/components/index.js | 2 - .../features/chat/components/index.native.js | 3 + react/features/chat/components/index.web.js | 3 + react/features/chat/components/native/Chat.js | 168 ++++++++++++++++++ .../chat/components/native/ChatButton.js | 129 ++++++++++++++ .../chat/components/native/ChatMessage.js | 152 ++++++++++++++++ .../features/chat/components/native/index.js | 4 + .../features/chat/components/native/styles.js | 124 +++++++++++++ .../components/{Chat.web.js => web/Chat.js} | 131 ++------------ .../ChatCounter.js} | 2 +- .../{ChatInput.web.js => web/ChatInput.js} | 2 +- .../ChatMessage.js} | 30 +--- .../DisplayNameForm.js} | 4 +- .../SmileysPanel.js} | 2 +- react/features/chat/components/web/index.js | 4 + react/features/chat/middleware.js | 99 +++++++---- .../chat/{sounds.web.js => sounds.js} | 0 .../components/Conference.native.js | 3 + .../components/DisplayNamePrompt.native.js | 30 ++++ .../toolbox/components/native/OverflowMenu.js | 2 + .../toolbox/components/native/Toolbox.js | 38 +++- .../toolbox/components/native/styles.js | 10 ++ 43 files changed, 1015 insertions(+), 199 deletions(-) create mode 100644 react/features/chat/components/AbstractChat.js create mode 100644 react/features/chat/components/AbstractChatMessage.js delete mode 100644 react/features/chat/components/Chat.native.js delete mode 100644 react/features/chat/components/ChatCounter.native.js delete mode 100644 react/features/chat/components/ChatInput.native.js delete mode 100644 react/features/chat/components/ChatMessage.native.js delete mode 100644 react/features/chat/components/DisplayNameForm.native.js delete mode 100644 react/features/chat/components/SmileysPanel.native.js delete mode 100644 react/features/chat/components/index.js create mode 100644 react/features/chat/components/index.native.js create mode 100644 react/features/chat/components/index.web.js create mode 100644 react/features/chat/components/native/Chat.js create mode 100644 react/features/chat/components/native/ChatButton.js create mode 100644 react/features/chat/components/native/ChatMessage.js create mode 100644 react/features/chat/components/native/index.js create mode 100644 react/features/chat/components/native/styles.js rename react/features/chat/components/{Chat.web.js => web/Chat.js} (63%) rename react/features/chat/components/{ChatCounter.web.js => web/ChatCounter.js} (95%) rename react/features/chat/components/{ChatInput.web.js => web/ChatInput.js} (99%) rename react/features/chat/components/{ChatMessage.web.js => web/ChatMessage.js} (87%) rename react/features/chat/components/{DisplayNameForm.web.js => web/DisplayNameForm.js} (96%) rename react/features/chat/components/{SmileysPanel.web.js => web/SmileysPanel.js} (97%) create mode 100644 react/features/chat/components/web/index.js rename react/features/chat/{sounds.web.js => sounds.js} (100%) diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index 3fc2d1ce6..c0b90fac0 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -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") diff --git a/css/_font.scss b/css/_font.scss index 794d78acc..c3b0283a2 100644 --- a/css/_font.scss +++ b/css/_font.scss @@ -25,6 +25,9 @@ -moz-osx-font-smoothing: grayscale; } +.icon-chat-unread:before { + content: "\e0b7"; +} .icon-arrow_back:before { content: "\e5c4"; } diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index 3e5e995ed253fdc8981e8226b303751c19378802..a99abba141a0f0f70b37c50f31e6556eccd845bb 100755 GIT binary patch delta 594 zcmdm!yds5F!-#>QXCkXP%j&aU+a@~H)PGcZx}1T5QHFtm;ZJ&EaRHDPU|?X90MZ=k zIhAP({~XB$@)Z~uw5MdGCZ>q6YH%|!=xzbZn`Hn6I8s;+1Nm=&e3gvck_z>MvR4@x z^nhkC8sy|BC)U3&{>Q+e?*Npam77>mz!1&=RIh&r$XCcq%#}>NJE0cn0R2Be4S5Cm z#U%{PKq&^poXH`K&4LerOcMqL1{MYp21W*j2iqqD&lK3R&%Sl?IPLhhhktjrG? zM(Jl#9#RfcHd5A7ypsPU|4M$B;1aJ8PZ5{k|HuEAe-+S7#>tILI+G_baZX;#G?%%Ehn`5hCRw#+uKv#aCzZN4&avw%Fq zaKo{%14d64&@&hHfrx`8fu3Uk;jqmSdg_ezJ`6n!%NX7;>M=SohA|#syutX1iHFIF zsew6(C5L4hD<5kW>nb)jwlcO$>`Lq<>>cb+I5;@8IHEXiaeU*H;dJ4g#`%WJimQq1 z9=8d15%&e22%Z+6bvze%{_wi+9^-w*r@&Xj*Ty%C?+iZ&e-Zx&ff)j~1Pui11b+$D u2>ntL4iJ7LGDGBysDh}CXq4zFpyL>U*oWaD0~gr6Ou-mGuX%Lo9LrI!%^ delta 472 zcmZ3HvL~5!k0Aqt!bDbcmiF+}potDO^IK>iw_ znL0W7$%(nOl}QW?dN+XbGjbCv3K+r|4l^+57XbMRd5O7_sdp#TE(Z!s0BXo9$S*Eo zUMi0l65N?=lS1Po>{ z1!NJ!4b^^e7(KZ}&s@+OA`X@WTFk((`GB4}V|^S$1;ai@21XG^4aP}~s~C?k-eVGB z3Su^4v16%VdBEz!TElvS&4X_9*sk?6=teaOiNX;W)wZj#G>?gL5Aj4_64+ z25vTP7w$zoT0CJq4Lpl@&hd)z&fwj{`-#tuFM= + diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 36a98c788562e431a18a4ba7f4528f38006ecd93..a624768c840e5d6ddb21c9e7b471e885cb32b34a 100755 GIT binary patch delta 578 zcmZ3G)RR0xq5h-N)8z~dj4})i41dxSiwl6X00RS)1d!%PpX_~%G2kgveNpgkoc zH8DkmRfC&>L3ayK-Yf$sz>&go7|4GEtsPDmq-?dForPZRm>n6b_Q++NucGX zilQKFDynEIY$PTsD8iAgGTXe)u8!xo`O3h}0`d#P4adR`7(KZ}*Id*GA`X@W zdX52v!#3~HRcEaCVd!C4#_)zwkI{)SjPU^D4aQGQJWNhZ4a`X_IV{Uq`Bu9mfd7J`4vLxWMjZ0*0jU=E?fKi~v=flRN+b delta 466 zcmeB4UXV0Fp`whEOsmvEDus|EViR@b>;C`+{(Mw=x;#CxxPXCyDFw*q zfMS94oXRwySPlb&_7o6a_~%G&MrtBZjULDxGY}SG)!@zm3IfF}fP57Y=15^VoRM2n z0Thd2U|=)=VfBNuS99`{f$H>ofNExeaQ%Cu;(xh`6+nv(G=KsMU>weHI4?0b6)5Hb z)R+gtcPG>?FUT)00s3Lb#6R7F4}c0yfXZ1IM1WpXc(8r)G)7a#?UPS28tePYTgV-h zi@~>iG1_y-BWCteB$+1i> zkt_^h3}MWxm_ah^z`&9OT5hT+3c{wMil)LwVxoc~Y|4u4f+l9h%nbh+7{mUpVqyqW zGBQ;DyK0r8GSjNCu>TBUVb{XK7_|(Q|1l_;tYT(RGTJWuY13_T3X7~U}IF*-4ZF&<#N!T5=Z zhslYlfjNmKhh-TnA8QrsDmFH@GPXf@p^V&; z3ZU2z1_nkQ5LP}YTc4Ak3{$l3bj{@s$i>Kfmtl~8 zD&;O^FJ&!dCB-B8SMrbKHwjMha`9wwasI#jfB082FoT1@cycO}%Vt()W^I|E)bRFr zew(ih+$=!D85nM;_KU;l$trs0g5D5uuq4o828PWAdg_ezaSRm<`xqG*MHn?0Co!&K zJi>U7NrWkg*@VT8rGn)Fs}E}p>kT#!wk2#|*xlHp*tfCYV*kUT!?A|r1jjp0G0qIm zeOx?TAzT}{*|=S}7x8HEgz+@+EaExGE5r_zwwW39J!h n5)2SLBjhJ^Mp#Kic%Mj?$UKoxqI{w{qI19z?Y&vgU="; }; 75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = ""; }; 75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = ""; }; + 87FE6F3221E52437004A5DC7 /* incomingMessage.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = incomingMessage.wav; path = ../../sounds/incomingMessage.wav; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; A4414ADF20B37F1A003546E6 /* rejected.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = rejected.wav; path = ../../sounds/rejected.wav; sourceTree = ""; }; @@ -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 */, diff --git a/lang/main.json b/lang/main.json index 611c17727..7fb3b8b7e 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 06707a055..a9a9ae317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ba316927c..7bac4f973 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/react/features/chat/actionTypes.js b/react/features/chat/actionTypes.js index ce699ae92..91ae0ba91 100644 --- a/react/features/chat/actionTypes.js +++ b/react/features/chat/actionTypes.js @@ -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 diff --git a/react/features/chat/actions.js b/react/features/chat/actions.js index 8f2ba9c5c..bff2c953f 100644 --- a/react/features/chat/actions.js +++ b/react/features/chat/actions.js @@ -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 diff --git a/react/features/chat/components/AbstractChat.js b/react/features/chat/components/AbstractChat.js new file mode 100644 index 000000000..bdcd5c845 --- /dev/null +++ b/react/features/chat/components/AbstractChat.js @@ -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, + + /** + * 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 extends Component

{} + +/** + * 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, + * _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 + }; +} diff --git a/react/features/chat/components/AbstractChatMessage.js b/react/features/chat/components/AbstractChatMessage.js new file mode 100644 index 000000000..33fbc70c1 --- /dev/null +++ b/react/features/chat/components/AbstractChatMessage.js @@ -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 extends PureComponent

{} + +/** + * 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) + }; +} diff --git a/react/features/chat/components/Chat.native.js b/react/features/chat/components/Chat.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/chat/components/ChatCounter.native.js b/react/features/chat/components/ChatCounter.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/chat/components/ChatInput.native.js b/react/features/chat/components/ChatInput.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/chat/components/ChatMessage.native.js b/react/features/chat/components/ChatMessage.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/chat/components/DisplayNameForm.native.js b/react/features/chat/components/DisplayNameForm.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/chat/components/SmileysPanel.native.js b/react/features/chat/components/SmileysPanel.native.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/chat/components/index.js b/react/features/chat/components/index.js deleted file mode 100644 index c8f9441e1..000000000 --- a/react/features/chat/components/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export Chat from './Chat'; -export ChatCounter from './ChatCounter'; diff --git a/react/features/chat/components/index.native.js b/react/features/chat/components/index.native.js new file mode 100644 index 000000000..a32ec6061 --- /dev/null +++ b/react/features/chat/components/index.native.js @@ -0,0 +1,3 @@ +// @flow + +export * from './native'; diff --git a/react/features/chat/components/index.web.js b/react/features/chat/components/index.web.js new file mode 100644 index 000000000..40d5f4652 --- /dev/null +++ b/react/features/chat/components/index.web.js @@ -0,0 +1,3 @@ +// @flow + +export * from './web'; diff --git a/react/features/chat/components/native/Chat.js b/react/features/chat/components/native/Chat.js new file mode 100644 index 000000000..4484f5a14 --- /dev/null +++ b/react/features/chat/components/native/Chat.js @@ -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 { + + /** + * 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 ( + +

+ + +
+ + + + + ); + } + + _onSend: (Array) => 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 ( + + ); + } + + _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)); diff --git a/react/features/chat/components/native/ChatButton.js b/react/features/chat/components/native/ChatButton.js new file mode 100644 index 000000000..38c3e9058 --- /dev/null +++ b/react/features/chat/components/native/ChatButton.js @@ -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 { + 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); diff --git a/react/features/chat/components/native/ChatMessage.js b/react/features/chat/components/native/ChatMessage.js new file mode 100644 index 000000000..e8f3aa617 --- /dev/null +++ b/react/features/chat/components/native/ChatMessage.js @@ -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 { + /** + * 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 ( + + { + + // Avatar is only rendered for remote messages. + !localMessage && this._renderAvatar() + } + + + { + + // Display name is only rendered for remote + // messages. + !localMessage && this._renderDisplayName() + } + + { message.text } + + + + { timeStamp } + + + + ); + } + + /** + * Renders the avatar of the sender. + * + * @returns {React$Element<*>} + */ + _renderAvatar() { + const { _avatarURL } = this.props; + + return ( + + + + ); + } + + /** + * Renders the display name of the sender. + * + * @returns {React$Element<*>} + */ + _renderDisplayName() { + const { message } = this.props; + + return ( + + { message.user.name } + + ); + } +} + +/** + * 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)); diff --git a/react/features/chat/components/native/index.js b/react/features/chat/components/native/index.js new file mode 100644 index 000000000..2d8300bd0 --- /dev/null +++ b/react/features/chat/components/native/index.js @@ -0,0 +1,4 @@ +// @flow + +export { default as Chat } from './Chat'; +export { default as ChatButton } from './ChatButton'; diff --git a/react/features/chat/components/native/styles.js b/react/features/chat/components/native/styles.js new file mode 100644 index 000000000..ee3e2b09b --- /dev/null +++ b/react/features/chat/components/native/styles.js @@ -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 + } +}); diff --git a/react/features/chat/components/Chat.web.js b/react/features/chat/components/web/Chat.js similarity index 63% rename from react/features/chat/components/Chat.web.js rename to react/features/chat/components/web/Chat.js index 2b7418ba3..509c387bf 100644 --- a/react/features/chat/components/Chat.web.js +++ b/react/features/chat/components/web/Chat.js @@ -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, - - /** - * 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 { - - /** - * Reference to the HTML element used for typing in a chat message. - */ - _chatInput: ?HTMLElement; +class Chat extends AbstractChat { /** * Whether or not the {@code Chat} component is off-screen, having finished @@ -96,15 +42,12 @@ class Chat extends Component { 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 { ); } - _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 {
{ messages }
- + ); } @@ -213,14 +145,14 @@ class Chat extends Component { _renderPanelContent(state) { this._isExited = state === 'exited'; - const { _isOpen, _showNamePrompt } = this.props; + const { _isOpen, _onToggleChat, _showNamePrompt } = this.props; const ComponentToRender = !_isOpen && state === 'exited' ? null : (
X
+ onClick = { _onToggleChat }>X
{ _showNamePrompt ? : this._renderChat() } @@ -256,20 +188,6 @@ class Chat extends Component { } } - _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 { } } -/** - * 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, - * _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)); diff --git a/react/features/chat/components/ChatCounter.web.js b/react/features/chat/components/web/ChatCounter.js similarity index 95% rename from react/features/chat/components/ChatCounter.web.js rename to react/features/chat/components/web/ChatCounter.js index b5e1caadb..a267373dc 100644 --- a/react/features/chat/components/ChatCounter.web.js +++ b/react/features/chat/components/web/ChatCounter.js @@ -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}. diff --git a/react/features/chat/components/ChatInput.web.js b/react/features/chat/components/web/ChatInput.js similarity index 99% rename from react/features/chat/components/ChatInput.web.js rename to react/features/chat/components/web/ChatInput.js index 779617448..f5085d098 100644 --- a/react/features/chat/components/ChatInput.web.js +++ b/react/features/chat/components/web/ChatInput.js @@ -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'; diff --git a/react/features/chat/components/ChatMessage.web.js b/react/features/chat/components/web/ChatMessage.js similarity index 87% rename from react/features/chat/components/ChatMessage.web.js rename to react/features/chat/components/web/ChatMessage.js index 9ed51ff45..33f91fb8e 100644 --- a/react/features/chat/components/ChatMessage.web.js +++ b/react/features/chat/components/web/ChatMessage.js @@ -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 { +class ChatMessage extends AbstractChatMessage { /** * Implements React's {@link Component#render()}. * diff --git a/react/features/chat/components/DisplayNameForm.web.js b/react/features/chat/components/web/DisplayNameForm.js similarity index 96% rename from react/features/chat/components/DisplayNameForm.web.js rename to react/features/chat/components/web/DisplayNameForm.js index 1e2098ad5..05824d3a0 100644 --- a/react/features/chat/components/DisplayNameForm.web.js +++ b/react/features/chat/components/web/DisplayNameForm.js @@ -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}. diff --git a/react/features/chat/components/SmileysPanel.web.js b/react/features/chat/components/web/SmileysPanel.js similarity index 97% rename from react/features/chat/components/SmileysPanel.web.js rename to react/features/chat/components/web/SmileysPanel.js index 8ebcc089b..17ff86b6e 100644 --- a/react/features/chat/components/SmileysPanel.web.js +++ b/react/features/chat/components/web/SmileysPanel.js @@ -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}. diff --git a/react/features/chat/components/web/index.js b/react/features/chat/components/web/index.js new file mode 100644 index 000000000..f48310329 --- /dev/null +++ b/react/features/chat/components/web/index.js @@ -0,0 +1,4 @@ +// @flow + +export { default as Chat } from './Chat'; +export { default as ChatCounter } from './ChatCounter'; diff --git a/react/features/chat/middleware.js b/react/features/chat/middleware.js index 65249959d..11b7a46f5 100644 --- a/react/features/chat/middleware.js +++ b/react/features/chat/middleware.js @@ -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)); + } } ); } diff --git a/react/features/chat/sounds.web.js b/react/features/chat/sounds.js similarity index 100% rename from react/features/chat/sounds.web.js rename to react/features/chat/sounds.js diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index 8fd750c27..652385e63 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -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 { hidden = { true } translucent = { true } /> + + {/* * The LargeVideo is the lowermost stacking layer. */ diff --git a/react/features/display-name/components/DisplayNamePrompt.native.js b/react/features/display-name/components/DisplayNamePrompt.native.js index e69de29bb..2f514529a 100644 --- a/react/features/display-name/components/DisplayNamePrompt.native.js +++ b/react/features/display-name/components/DisplayNamePrompt.native.js @@ -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 ( + + ); + } + + _onSetDisplayName: string => boolean; +} + +export default connect()(DisplayNamePrompt); diff --git a/react/features/toolbox/components/native/OverflowMenu.js b/react/features/toolbox/components/native/OverflowMenu.js index 6ee82c614..427e1de27 100644 --- a/react/features/toolbox/components/native/OverflowMenu.js +++ b/react/features/toolbox/components/native/OverflowMenu.js @@ -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 { } + ); diff --git a/react/features/toolbox/components/native/Toolbox.js b/react/features/toolbox/components/native/Toolbox.js index 55478a5f3..8fc0560ed 100644 --- a/react/features/toolbox/components/native/Toolbox.js +++ b/react/features/toolbox/components/native/Toolbox.js @@ -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 { 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 { - + diff --git a/react/features/toolbox/components/native/styles.js b/react/features/toolbox/components/native/styles.js index 60861e303..7483ced23 100644 --- a/react/features/toolbox/components/native/styles.js +++ b/react/features/toolbox/components/native/styles.js @@ -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 + } +});