Compare commits

..
1629 Commits
Author SHA1 Message Date
or-else ebe47b345d pix editing ofpinned messages 2023-04-05 10:14:07 -07:00
or-else 454dc204c8 minor visual tweaks 2023-03-25 11:13:04 -07:00
or-else d0c88fbd0d finishing touches, all seems to work 2023-03-24 15:15:40 -07:00
or-else e68ebc8336 works as expected on desktop, mobile needs work 2023-03-24 13:56:24 -07:00
or-else cca97d410e widget for pinned messages 2023-03-23 18:22:30 -07:00
or-else eafd87fb17 make bg change on click more subtle 2023-03-23 18:16:32 -07:00
or-else 71ee3fbec4 widget for showing pinned messages (wip) 2023-03-23 18:09:11 -07:00
or-else 11fe81fdf1 adding pinned messages (wip) 2023-03-22 17:50:33 -07:00
or-else 9640c604b9 Merge branch 'devel' into next 2023-03-22 11:00:37 -07:00
or-else 6273b3287f rebuilt with correct version of the sdk 2023-03-22 09:22:39 -07:00
or-else 9c5e3a3eca dependencies updated 2023-03-22 09:19:31 -07:00
or-else 9098c1f6a6 merge devel 2023-03-20 17:21:25 -07:00
or-else a2439be540 Merge branch 'devel' into next 2023-03-20 16:12:41 -07:00
or-else a2fb3fe1a0 rebuilt 2023-03-20 16:12:36 -07:00
or-else 560e3369e4 rebuilt 2023-03-20 16:11:43 -07:00
or-else 491175cecc dependencies updated 2023-03-20 16:10:36 -07:00
or-else 6d96d327f1 fix backward scrolling on fetching older messages 2023-03-20 11:35:01 -07:00
or-else 6ee2851943 rebuilt 2023-03-19 15:14:24 -07:00
or-else 192a3d4ec2 don't crash if FIREBASE_INIT is not defined 2023-03-19 15:14:00 -07:00
or-else c6f7911c82 dependencies updated, version to 0.22.0, rebuilt 2023-03-15 10:43:29 -07:00
or-else 9a1f0c734a dependencies updated 2023-03-15 08:41:22 -07:00
or-else 52f879490e fix for tinode/chat/issues/838 2023-03-14 18:14:33 -07:00
or-else 8df3c34cf4 moved hiding components to the top level 2023-03-14 15:54:22 -07:00
or-else 2fb6a9bb0a escape key closes image, video, or document preview 2023-03-14 14:23:16 -07:00
or-else 5ae58fb2a6 allow message input textarea height to grow with more text 2023-03-14 13:53:21 -07:00
or-else 43798ea1d9 minor tweaks 2023-03-14 09:44:54 -07:00
or-else 323042bd96 rebuild 2023-03-03 15:56:09 -08:00
or-else 172b9eb26d removed duplicate styling 2023-03-03 15:52:53 -08:00
or-else 1b90c613f7 fix drag and drop in firefox 2023-03-02 16:19:13 -08:00
or-else c6b4460389 merge devel and bump version to 0.22.0-rc1 2023-03-02 13:32:42 -08:00
or-else 497318b722 updated dependencies and bumped version 2023-03-01 16:49:14 -08:00
or-else 423f53880d Merge branch 'devel' 2023-03-01 14:34:32 -08:00
or-else a93ede25e9 make date bubble lighter 2023-02-24 18:50:09 -08:00
or-else b4d3e0274f fix color of the date bubble 2023-02-24 16:23:22 -08:00
or-else 4f116ab3e6 replace box-shadow with filter: drop-shadow to remove artifacts 2023-02-24 15:39:08 -08:00
or-else 75b2880b63 bump version 2023-02-20 16:18:28 -08:00
or-else 1149980a47 rebuilt 2023-02-20 16:05:36 -08:00
aforge 87b48cd67d Fix strutil.secondsToTime(). 2023-02-16 13:26:55 -08:00
aforge 4be09a034e Calls: create data channel only on the caller's side.
Removes redundancy and a memory leak (and thus an error upon closing).
2023-02-16 13:25:15 -08:00
aforge 3c0b57333c Calls: create data channel only on the caller's side.
Removes redundancy and a memory leak (and thus an error upon closing).
2023-02-15 17:55:29 -08:00
Gene 0c9b23eee9 Merge pull request #118 from sun2008mt/master
Update zh.json
2023-02-09 08:33:58 -08:00
Marc SUN d11f94095f Update zh.json 2023-02-09 18:14:08 +08:00
or-else 133ed3cc53 merge devel 2023-02-08 13:40:09 -08:00
or-else 7bc50fac36 handing of "busy" webrtc status 2023-02-08 13:30:50 -08:00
aforge fbdd017d38 Fix strutil.secondsToTime(). 2023-02-06 22:35:48 -08:00
or-else 8fdb4a8a4c bump version to 0.21.1 and rebuild 2023-02-06 10:22:00 -08:00
or-else e1b1ae503f fix issues with video uploading and display 2023-02-06 09:58:56 -08:00
or-else 7b44913724 fix display of broken videos 2023-02-06 09:56:50 -08:00
or-else 7e3b2fc254 switch tinode-sdk from browserify to webpack 2023-02-06 09:54:12 -08:00
aforge fc6a8fbafc Do not crash in MessagesView.handleMessageUpdate if topic gets unset. 2023-02-06 09:51:53 -08:00
or-else 111b9d8641 rebuilt with the sdk fix 2023-02-05 17:49:31 -08:00
or-else eeea27ca82 fix issues with video uploading and display 2023-02-05 17:08:41 -08:00
or-else fc2ff79329 fix display of broken videos 2023-02-05 13:46:08 -08:00
or-else f755314c3b rebuilt 2023-02-05 12:43:17 -08:00
or-else caf02056ab merge next 2023-02-04 21:23:53 -08:00
or-else 4b26d5ed40 rebuilt 2023-02-04 21:23:02 -08:00
aforge 885d9932e2 Do not crash in MessagesView.handleMessageUpdate if topic gets unset. 2023-02-04 17:58:56 -08:00
or-else f6c355498b switch tinode-sdk from browserify to webpack 2023-02-04 14:31:36 -08:00
or-else 455f0f5c67 implement AsYouType in phone editing 2023-02-04 10:54:49 -08:00
or-else e99370683b merge next 2023-02-04 10:04:59 -08:00
or-else 9fb1551bef fix npm run clean 2023-02-04 09:55:10 -08:00
or-else 8d40301ddf Merge branch 'next' into devel 2023-02-03 16:56:58 -08:00
or-else 93fb2b8bca added missing translation 2023-02-03 11:35:36 -08:00
or-else 97178a603f dependencies updated (except firebase - the latest is broken), version bumped 2023-02-03 11:13:59 -08:00
or-else f13c1d21c0 merge next 2023-02-01 15:56:56 -08:00
or-else 7e28c8693c Merge branch 'devel' into next 2023-02-01 15:54:29 -08:00
or-else c95181b0cf cleanup of umd/* 2023-02-01 15:54:15 -08:00
or-else b2c5051d31 cleanup of umd/* 2023-02-01 15:53:18 -08:00
or-else 6e46a8dfda fixed + cleanup 2023-02-01 15:37:44 -08:00
or-else 99c9a698a0 password reset seems to work correctly 2023-02-01 14:38:57 -08:00
or-else aa5e9ef5c7 adding credential editing 2023-01-27 16:18:23 -08:00
or-else 4f6d617829 merge next 2023-01-27 09:03:09 -08:00
or-else 05c5ffa43c fix positioning of the peer's name tag in video calls 2023-01-26 17:32:54 -08:00
or-else 9d7bfec6d6 adding new files to git 2023-01-26 15:59:10 -08:00
or-else d8f7a41bdd password reset works, missing translations 2023-01-26 15:58:33 -08:00
or-else 6c36324ed4 phone number widget fixes 2023-01-25 18:41:19 -08:00
or-else e2f3201ddb phone editing widget works 2023-01-25 17:08:07 -08:00
or-else 34f17f45b1 Merge branch 'next' into phone 2023-01-25 11:32:24 -08:00
or-else 1cf4722830 fix: broken alert view 2023-01-25 11:29:26 -08:00
or-else 7c4327ac71 bump version to 0.21.0-rc2 2023-01-24 09:01:07 -08:00
or-else 48caa605c7 work in progress 2023-01-24 08:53:08 -08:00
aforge edb1383d60 Merge branch 'next' of https://github.com/tinode/webapp into next 2023-01-23 13:33:11 -08:00
aforge 2a653f2299 Fix missing audio. 2023-01-23 13:27:50 -08:00
or-else eba83fdec8 support for registering with a phone number 2023-01-23 10:48:43 -08:00
or-else 5f173dd395 merge next 2023-01-23 10:24:48 -08:00
or-else 2115f6d638 merge devel 2023-01-23 10:23:04 -08:00
or-else 5228d44ade rebuilt 2023-01-23 10:19:43 -08:00
or-else ce19ebbbdd fix for https://github.com/tinode/chat/issues/825 2023-01-23 10:18:22 -08:00
or-else 5e60a5e04d call parent on lost focuse 2023-01-23 10:00:07 -08:00
or-else 9f523b54a4 attempt to integrate phone editing 2023-01-22 20:04:13 -08:00
or-else 6c13943f1a forgot to add new files 2023-01-22 19:26:01 -08:00
or-else b0bf93a4d9 phone number editing controls are functional 2023-01-22 18:57:19 -08:00
aforge b766e827c9 Set data channel in state. 2023-01-18 14:10:49 -08:00
or-else 424a8b64bf remove autoFocus from message input field on mobile, #111 2023-01-17 12:50:58 -08:00
or-else 9108efc8c9 merge google analytics branch 2023-01-14 12:54:53 -08:00
or-else f94c3612be Merge branch 'next' into devel 2023-01-14 11:47:03 -08:00
or-else 95ddfd21eb version bumped to 0.21.0-rc1, rebuilt 2023-01-14 11:46:50 -08:00
Gene ae5618c072 Merge pull request #108 from tinode/audio-only
Audio-only calls
2023-01-14 11:09:51 -08:00
or-else d083a286df correct comment wording 2023-01-13 19:00:26 -08:00
or-else e0db170aec merge next 2023-01-13 17:28:06 -08:00
or-else e9d0f0f1f3 remove styling from focused buttons 2023-01-13 17:25:57 -08:00
or-else 08d4c08cbc optional integration of google analytics 2023-01-12 20:15:08 -08:00
or-else 15d24c6607 do not logout of server errors 500 or timeouts 2023-01-11 09:03:48 -08:00
or-else c085ef8ce1 make disabled buttons more obviously disabled 2023-01-10 17:59:12 -08:00
or-else 2d76befea1 fix missed calls when topic is not in focus 2023-01-10 17:18:34 -08:00
or-else 20c54fb7e3 removed debug logging 2023-01-10 16:44:03 -08:00
aforge c5834fad4d Merge pull request #109 from tinode/mute-video
Mute/unmute video properly.
2023-01-10 16:29:57 -08:00
or-else 640704ee2a bug fixes 2023-01-10 16:28:32 -08:00
aforge 18dd37b402 Fix remote video screen flickering. 2023-01-10 13:00:49 -08:00
aforge 54d6719307 Fix race between setState and PeerConnection events. 2023-01-09 20:23:26 -08:00
aforge 255e7fda98 Style + unmute delay fix. 2023-01-09 17:52:10 -08:00
aforge 1a6e8c8e74 Mute/unmute video properly.
Now muting turns the camera off instead of sending blank frames.
2023-01-09 16:08:44 -08:00
or-else b642d05a10 merge next 2023-01-09 11:56:46 -08:00
or-else 920619a988 make call buttons round for consistency with other apps 2023-01-09 11:55:56 -08:00
or-else acc24b552d simplify a bit and remove debug logging 2023-01-09 11:21:30 -08:00
or-else ba94788fbe fix service worker and notifications init for FF and Safari 2023-01-09 11:18:21 -08:00
or-else 2758bb9293 dependencies updated, version bumped to -beta2 2023-01-08 12:39:32 -08:00
or-else 4635456c1a rebuilt with changed SDK 2023-01-07 14:57:41 -08:00
or-else 89f9821255 audio-only calls kind of work 2023-01-07 14:56:07 -08:00
or-else 8690ae6a1b merge next 2023-01-06 10:48:25 -08:00
or-else 483d7ef58d Merge branch 'next' into devel 2023-01-05 18:46:19 -08:00
or-else e1d7ebcc22 rebuilt with the sdk 2023-01-05 18:46:16 -08:00
or-else 45fbff1b4a rebuilt with sdk fix: incorrect reference when replying to edited message 2023-01-05 17:20:20 -08:00
or-else f99f5a223f assign file size for in-band attachments 2023-01-05 13:22:08 -08:00
or-else c0e4cd4965 rebuilt with SDK fix: ensure edits cannot change message sender 2023-01-05 12:33:42 -08:00
Gene 7d796e3dae Merge pull request #106 from tinode/next
Message editing, video messages
2023-01-05 09:48:35 -08:00
or-else 1d3a9040cb rebuilt with the sdk fix for previews of forwarded messages 2023-01-04 21:00:12 -08:00
or-else 29cce0a4cc rebuilt with the sdk fix for previews of forwarded messages 2023-01-04 20:59:14 -08:00
or-else c59c3eeb0b merge next 2023-01-04 18:09:16 -08:00
or-else cab80e4d62 translations for the new error message 2023-01-04 18:04:15 -08:00
or-else f7c5322e36 clear "reply/editing/forwarding state when switching topics 2023-01-04 17:59:45 -08:00
or-else cb64e1f247 fix handling of inline videos 2023-01-04 17:52:28 -08:00
or-else afee6b5dd9 clear edited message text when switching topics 2023-01-04 13:58:19 -08:00
or-else 9f01eab324 disable editing quoted messages 2023-01-04 13:05:03 -08:00
or-else 80d48077c9 initial pass on audio-only calls 2023-01-04 12:47:02 -08:00
or-else 1bc6f87844 use "in progress" instead of 0:00 for calls in progress 2023-01-03 17:34:53 -08:00
or-else 0ea112b145 Merge branch 'devel' into next 2023-01-02 17:45:07 -08:00
or-else 768edd744d fallback to fixed size when video has no size info 2023-01-02 13:18:26 -08:00
or-else 1aadf4e7bd rebuilt with correct sdk version 2022-12-31 16:01:55 -08:00
or-else 2d92e475b5 dependencies updated and rebuilt 2022-12-31 13:55:38 -08:00
or-else a28c3eb5cd minor vidual correction 2022-12-31 13:48:57 -08:00
or-else 97208e1448 bum version to 0.20.4 2022-12-30 12:26:46 -08:00
or-else 9fbc601870 fix preview of received videos 2022-12-30 10:20:13 -08:00
or-else 4489646144 fix for a couple of minor problems 2022-12-20 18:34:20 -08:00
or-else 261b2dbdd7 minor color tweak 2022-12-16 09:28:13 -08:00
or-else 224323f443 merge devel 2022-12-16 09:15:07 -08:00
or-else 6d031f033e another small tweak to badges 2022-12-16 09:13:27 -08:00
or-else c38e1832dc tweak topic type badges 2022-12-16 09:11:05 -08:00
or-else 13036da36b more cleanup of video attachments 2022-12-14 21:05:40 -08:00
or-else f5c628fbb5 new translations 2022-12-14 19:39:10 -08:00
or-else 5673d82b33 replies to video recordings work 2022-12-14 19:24:59 -08:00
or-else 8c3287941a video works although may have bugs 2022-12-14 16:50:00 -08:00
or-else 830d15f7ec rebuilt 2022-12-14 14:27:56 -08:00
or-else 4e42d96d66 fix video message previews 2022-12-14 14:27:33 -08:00
or-else ed77f48189 move dnd to MessagesView, otherwise problems with layout 2022-12-14 14:26:47 -08:00
or-else ed3107405f make drag n drop prettier 2022-12-14 10:54:39 -08:00
or-else 03e87697cf merge with drag-n-drop 2022-12-13 17:38:07 -08:00
or-else bccf0d3b6b video uploading + preview in bubbles works 2022-12-13 17:37:04 -08:00
aforge 36b7d1b721 Merge pull request #104 from tinode/drag-and-drop
Drag & drop for attaching files.
2022-12-13 10:37:32 -08:00
aforge 2294410fb9 Merge branch 'next' into drag-and-drop 2022-12-13 10:36:47 -08:00
aforge 10140c6450 Move file attach DnD to MessagesView. 2022-12-13 00:20:36 -08:00
or-else 489a05663a adding widget for displaying video previes inside message bubbles 2022-12-12 22:09:17 -08:00
or-else f31a3ebdcc .gitattributes + *.min.css binary 2022-12-12 22:06:29 -08:00
or-else 6360708255 working on display of images 2022-12-12 22:05:08 -08:00
or-else 24a87f3deb videos work but with bugs 2022-12-12 18:37:59 -08:00
or-else a8d7e6be50 wip: adding support for video recordings 2022-12-12 16:48:15 -08:00
aforge b434c4ef17 Drag & drop for attaching files. 2022-12-12 16:32:48 -08:00
or-else 0d33c0f0a8 removed unused variables 2022-12-12 13:40:52 -08:00
or-else e95aea17be make date dividers reasier to read 2022-12-11 17:43:07 -08:00
or-else d820da2ee2 build with the SDK fix (timestamps of edited messages) 2022-12-10 18:37:07 -08:00
or-else 692e6e1d8f send "recording" notifications when audio is being recorded 2022-12-10 18:17:03 -08:00
or-else 6aca7e527e removed unused import 2022-12-08 14:07:24 -08:00
or-else ae34795c2d removed legacy translations 2022-12-08 14:05:28 -08:00
or-else 310b352a08 disable editing of some types of messages 2022-12-08 14:03:10 -08:00
or-else b8e9ca8d0a removed support for display of deleted message ranges 2022-12-08 12:32:30 -08:00
or-else 7731c6700b fix for https://github.com/tinode/chat/issues/808 2022-12-07 19:17:18 -08:00
or-else 563f82d265 merge devel 2022-12-07 18:29:28 -08:00
or-else 10b24cc16e updated dependencies, rebuilt, version bumped to 0.20.3 2022-12-06 15:19:27 -08:00
or-else f6cd3048e1 on-demand translation loading works now 2022-12-05 17:18:47 -08:00
or-else 4ad723b854 the app restored: "runtimeChunk: single" was the problem 2022-12-05 15:26:17 -08:00
or-else 4af7a39527 rebuilt 2022-12-05 14:03:13 -08:00
or-else 0e11ae9382 rebuilt 2022-12-05 13:58:48 -08:00
or-else a2014826e3 rebuilt, does not work 2022-12-05 09:58:38 -08:00
or-else 3a002f224d remove topLevelAwait experiment 2022-12-05 09:33:56 -08:00
or-else fa78e678b9 rebuilt 2022-12-05 09:24:38 -08:00
or-else 5fdcc65b5b does not work 2022-12-04 14:24:05 -08:00
or-else 476452c59e simplify package.json files section 2022-12-04 11:52:04 -08:00
or-else b0c0790b91 added new code chunks to package.json 2022-12-04 11:47:27 -08:00
or-else 9d83dee14c rebuilt with the webm fix & code splitting 2022-12-04 11:42:32 -08:00
or-else 09223c9efc adding newly split chunks 2022-12-04 11:25:42 -08:00
or-else d0a23b0792 use webm-duration-fix to add SeekHead, use code splitting 2022-12-04 11:25:06 -08:00
or-else d54e4a8153 does not work 2022-12-02 14:37:26 -08:00
or-else 57a062266f npm prune 2022-12-01 14:18:19 -08:00
or-else ed71c6bb6e extermalize fix-webm-duration, remove unused webrtc-adapter 2022-12-01 14:17:21 -08:00
or-else 307665d8ec merge devl 2022-11-26 14:23:01 -08:00
or-else 297aef7bf1 consisten avatar handling in new group creation 2022-11-26 14:21:07 -08:00
or-else 161f544442 explicitly set audio recording bit rate 2022-11-26 14:20:43 -08:00
or-else 22e782fa2d Revert "explicitly set audio recording bit rate"
This reverts commit 7135553e42.
2022-11-26 14:16:15 -08:00
or-else 7135553e42 explicitly set audio recording bit rate 2022-11-26 14:15:43 -08:00
or-else 1913ed0c11 fix for #102, part 2 2022-11-23 10:07:20 -08:00
or-else 6de41f4988 fix for #102, part 1 2022-11-23 10:00:41 -08:00
or-else c5e744aacf fix deletion of edited messages 2022-11-21 16:11:12 -08:00
or-else 983de97d7f removed StrictMode due to issues with double mounting of components 2022-11-20 16:20:53 -08:00
or-else 7236c913da shorten edit preview 2022-11-20 13:36:08 -08:00
or-else 72ec3b51a0 edited for code style consistency 2022-11-19 17:14:43 -08:00
or-else 3653453897 a couple of bugs in message editing 2022-11-19 16:43:57 -08:00
or-else d32c1636ce editing once works, editing twice does not 2022-11-19 16:21:24 -08:00
or-else a99ad81196 all dependencies updated to latest, including breaking changes 2022-11-17 11:43:28 -08:00
or-else eec52a590c bumped version to 0.20.2, dependencies updated 2022-11-16 09:33:33 -08:00
or-else c7b8ce11e0 force reconnect if countdown is negative (computer woke up from sleep) 2022-11-14 17:20:07 -08:00
or-else 2156aa0877 wrap long text in <tt> 2022-11-06 12:08:02 -08:00
or-else 966dec9654 rebuilt 2022-11-02 17:48:08 -07:00
or-else 0b2ab61422 dependencies updated, version bumped to 0.20.1 2022-11-02 17:46:21 -07:00
or-else 092b4697a9 releasing 0.20.0 2022-10-11 11:27:02 +03:00
or-else 6c5fe836f4 rebuilt with fix for #101 2022-09-22 14:45:43 +03:00
or-else 40b99f7332 update dependencies, bump version to 0.20.0-rc2, rebuild 2022-09-06 15:22:27 -07:00
or-else d45a141704 fix avatar upload on signup 2022-09-01 11:12:09 -07:00
or-else ee106a17f6 incomplete fix for authentication after credential validation, #100 2022-08-29 18:37:10 -07:00
or-else 22320808fc fix frozen validation input field 2022-08-27 12:51:52 -07:00
or-else 5006a6fb34 partial fix for avatar uploads 2022-08-25 18:50:11 -10:00
or-else df203f48bd fix display of a validation success message 2022-08-25 15:02:11 -10:00
or-else 0e53234d02 add "success" message when credential was validated, #100 2022-08-25 13:45:37 -10:00
or-else 14a4725c94 Merge branch 'next' of https://github.com/tinode/webapp into next 2022-08-25 13:03:44 -10:00
or-else 740cb7faf8 partial fix for #100 2022-08-25 13:03:31 -10:00
aforge 384955777d Update host when user hits Enter in host selector. 2022-08-24 12:36:10 +03:00
or-else 8b80f59d3a fix for a crash on a typo in a name of a variable 2022-08-23 18:12:51 -10:00
or-else 6a6e3ad754 restrict app width on very wide monitors 2022-08-22 12:11:18 -10:00
or-else d15aa9372d minor visual tweaks and missing translation 2022-08-22 12:05:32 -10:00
or-else 5144f581e7 better handling of non-default hosts, like connecting to sandbox from localhost 2022-08-15 15:06:33 -07:00
or-else 8250c5f372 enable connections to sandbox from localhost 2022-08-14 16:01:09 -07:00
or-else ae26e0ba3d added "Helping out" section 2022-08-09 18:04:32 -07:00
or-else 6a52591e1a updated dependencies and bumped version to -rc1, rebuilt 2022-08-09 16:02:19 -07:00
or-else 81afca1efe merge with remote 2022-08-09 15:16:43 -07:00
or-else eac59ea11f rebuilt with the sdk rc1 2022-08-09 15:14:45 -07:00
or-else 2f3a066838 disable video calling when no ICE config 2022-08-09 14:37:21 -07:00
aforge 3922db3aa6 Update call status text. 2022-08-02 13:43:57 +03:00
aforge 9cd778534a Add onIceCandidateError handler for video calls. 2022-07-26 01:07:26 +03:00
aforge 69282bab89 Add missing Tinode.onData and .onInfo handlers. 2022-07-25 12:29:03 +03:00
or-else af08392260 merge conflict resolved 2022-07-19 08:55:44 -07:00
or-else a90f1f4fef fix crash on missing AccessMode module 2022-07-19 08:54:49 -07:00
aforge 781009b745 Drop unsent video call messages when synching topics. 2022-07-17 17:03:27 +03:00
or-else 6357f9227e fix password focus, bump version to -beta3 2022-07-15 11:41:51 -07:00
aforge 7dfb90d8f3 Call establishment improvements.
1. More orderly offer-answer exchange sequence.
2. Cache remote ICE candidates that arrive before call establishment is complete (we use trickle ICE).
2022-07-06 16:44:19 +03:00
or-else 7348fdef7f rebuilt with fixed sdk 2022-07-02 15:11:16 -07:00
or-else e42de09504 rebuilt with fixed sdk 2022-07-02 15:09:51 -07:00
or-else 5c842ef212 bump version and rebuild 2022-06-30 11:29:04 -07:00
or-else c94b397222 merge fixes from devel 2022-06-30 11:05:53 -07:00
or-else c013885bec rebuild with the SDK fix + better error reporting 2022-06-30 10:54:33 -07:00
or-else 4a2667015f merged fix of password change 2022-06-29 18:40:16 -07:00
or-else e209ef2483 fix crash on attempt to change password 2022-06-29 18:39:00 -07:00
or-else 2a00791a1b simplified code a little bit 2022-06-29 12:20:43 -07:00
or-else dcf9e1d47e use topic.latestMsgVersion instead of iteration over versions 2022-06-29 10:15:57 -07:00
or-else 6c0612dc8f releasing v0.19.2 2022-06-28 14:37:15 -07:00
aforge 315631f67c Treat new webrtc:started messages as call invite.
In order to reduce redundancy, {info what=call event=invite}
will not be used as call invite any longer (and will be dropped soon).
2022-06-28 21:34:47 +03:00
or-else c31c0c2c07 fix clipping of a host name 2022-06-27 17:48:38 -07:00
aforge c7c2cffbbf Reject new incoming calls when a call is in progress. 2022-06-27 18:26:21 +03:00
or-else e77e8441cf recv ackns moved to the SDK 2022-06-26 19:29:51 -07:00
or-else d95c53ed24 add catch() to play(), otherwise it throws 2022-06-26 19:29:11 -07:00
or-else 300b4f8d64 typo 2022-06-23 16:23:22 -07:00
or-else 36977b429b small UI and metadata fixes 2022-06-23 13:23:06 -07:00
or-else 0d7643ee42 add video call screenshot 2022-06-19 16:43:43 -07:00
or-else 145ac78de8 bump version to 0.20.0-beta2 and rebuild 2022-06-19 12:55:34 -07:00
or-else 245603e8b9 dialing sound added 2022-06-19 12:53:09 -07:00
or-else 3eab7ec09b support for "ringing" 2022-06-19 11:52:07 -07:00
or-else a50261ab42 Merge branch 'master' into devel 2022-06-16 12:45:53 -07:00
or-else 8cd8ffb659 prevent crash when app is run under http, #763 2022-06-16 12:43:40 -07:00
Gene 932ab0a047 Ask to use index-dev.html instead of index.html 2022-06-16 19:44:00 +03:00
or-else b5686f45b3 bump version to 0.19.1 and rebuild 2022-06-14 09:32:46 -07:00
or-else 88c4cb42cd fix scroll bug in messages panel 2022-06-13 21:39:20 -07:00
or-else cd6a87037d bug fix: keep message panel at the bottom on new messages 2022-06-13 21:37:15 -07:00
or-else deaea078ea rebuilt 2022-06-13 15:32:32 -07:00
or-else bdfe163ecf clean up 2022-06-13 15:32:10 -07:00
or-else 1fb3a233a2 updated dependencies and rebuilt 2022-06-13 10:19:47 -07:00
or-else 0c91e4c615 merge devel 2022-06-13 09:04:52 -07:00
or-else b138fa2c44 mention french translation 2022-06-13 08:56:32 -07:00
or-else 70c4a775e3 rebuilt with french translation 2022-06-13 08:53:17 -07:00
Gene 978e858592 Merge pull request #99 from RomainStorai/frenchTranslation
Add French translation
2022-06-13 08:50:17 -07:00
Romain eca15317ce added fr to service worker 2022-06-13 16:49:50 +02:00
Romain a9961c2cc1 added french translation to i18n 2022-06-13 16:48:43 +02:00
or-else 557cbffe48 rebuilt 2022-06-12 16:11:25 -07:00
or-else 31258a75fa minor UI tweak 2022-06-10 19:28:07 -07:00
or-else f884874d43 incorporate "missing" and "declined" call statuses 2022-06-09 11:29:33 -07:00
or-else 762ccc1f31 rebuilt 2022-06-07 16:31:06 -07:00
or-else 10b3268d43 releasing 0.19.0 2022-06-05 13:41:22 -07:00
aforge fd1c13830c Stop ring sound after outgoing call has been accepted. 2022-06-04 21:49:01 +03:00
or-else 4e720a0d5b provide UI feedback for remote hangup 2022-06-03 16:50:06 -07:00
or-else 6dc91ad518 bump version to 0.19.0-rc2 2022-06-02 18:50:16 -07:00
or-else ae15a043b1 adding "end of call" sound 2022-06-02 18:46:33 -07:00
or-else 3af0b4409c bug fixes 2022-06-02 09:27:54 -07:00
or-else feb2e4b3e3 icon positioning fix 2022-06-01 11:33:51 -07:00
or-else 276d8c631a use server-provided ICE servers 2022-05-31 13:50:16 -07:00
or-else 720c9ae2d4 rebuilt with the sdk fix 2022-05-31 08:53:23 -07:00
or-else ed7e246f61 rebuilt with the sdk fix 2022-05-31 08:52:25 -07:00
or-else 0dea77b5f2 bump version to -beta1 2022-05-30 20:00:18 -07:00
or-else e287755f13 more idimatic VC formatting, better presentation of preview 2022-05-30 19:51:58 -07:00
or-else eef880c448 consistent naming or properties 2022-05-30 15:19:52 -07:00
or-else 1159efde80 bump version to v0.19.0-rc1 and rebuild 2022-05-30 10:26:57 -07:00
or-else 9507aeb28d screenshots updated with the audio message 2022-05-29 17:01:45 -07:00
or-else 95011e4a04 rebuilt 2022-05-29 16:42:44 -07:00
aforge fc94d7454f Merge pull request #97 from tinode/calls-head
Move video call message state and duration to head.
2022-05-29 23:54:49 +03:00
aforge 9dc1f6f3a7 Remove separate mime for video call messages. 2022-05-29 23:44:31 +03:00
aforge 3c87341280 Move video call message state and duration to head.
'webrtc' header for state.
'webrtc-duration' header for duration.

Content is now Drafty.videoCall() / VC.
2022-05-29 15:29:13 +03:00
or-else df460c6b61 a couple more UI tweaks 2022-05-28 18:50:11 -07:00
or-else fec6a5bfa6 call status in message preview was isolated in a class and made prettier 2022-05-28 18:36:54 -07:00
or-else 43e3395350 i18n of video calls 2022-05-28 16:54:43 -07:00
or-else 535d276665 call in progress animation, crash if button is clicked too soon 2022-05-28 14:42:06 -07:00
or-else b37ce5e8cf consistent capitalisation of menu items 2022-05-27 17:07:58 -07:00
or-else 147dfe0ea3 adding missing file 2022-05-27 17:05:52 -07:00
or-else 91eacd23ef nicer message bubbles for calls 2022-05-27 16:59:23 -07:00
or-else 97f3699e97 a small glitch 2022-05-27 15:17:00 -07:00
or-else 50c8c61078 made incoming calls panel prettier 2022-05-27 15:09:53 -07:00
or-else ff605369be correct method name 2022-05-27 11:16:37 -07:00
or-else 3d5b2bf7b3 add rining to outgoing call, convert souds to much smaller m4a 2022-05-27 11:13:07 -07:00
or-else 92aa11be39 outgoing call panel looks good 2022-05-27 10:40:38 -07:00
or-else 4e7c3419c0 make self-video mirrored 2022-05-26 21:47:17 -07:00
or-else feae1c9f46 package all sounds 2022-05-26 20:50:34 -07:00
or-else 293d71dea6 much smaller ring file, UI tweaks 2022-05-26 20:28:40 -07:00
or-else 7d2c7a634c a bit more work on sizing video 2022-05-26 19:44:58 -07:00
or-else d78dc0e22a making call UI prettier 2022-05-26 18:36:11 -07:00
or-else 203612b421 rebuilt with the sdk fix 2022-05-26 16:42:20 -07:00
or-else f40fd295b1 rebuilt 2022-05-26 14:40:04 -07:00
or-else b26239f1ff bump version to -beta2 2022-05-26 14:20:29 -07:00
or-else 13c1fcf811 merged with next 2022-05-26 10:16:09 -07:00
or-else a7d98136c5 interaction of push notification with tinode moved to the SDK 2022-05-25 14:22:56 -07:00
or-else 2a7c85c61c rebuilt with sdk changes 2022-05-25 10:56:26 -07:00
or-else 4ff315329c add a shim for "sub" push notifications 2022-05-24 20:21:49 -07:00
or-else 2b51f19fbd rebuilt 2022-05-23 20:49:50 -07:00
or-else a9677cee8b minor visual fixes for deleted topics 2022-05-23 15:47:34 -07:00
or-else 46b6a816eb more usable [scroll to bottom] button 2022-05-19 18:21:35 -07:00
or-else 6ffc03e8bd rebuilt 2022-05-18 14:32:18 -07:00
or-else ccfa7dacdf rebuilt 2022-05-18 13:59:38 -07:00
or-else ec01ac2a50 fixed pasting files and images 2022-05-17 15:53:11 -07:00
or-else a9e4f644a2 fix for breakage around forwarding review 2022-05-12 18:29:21 -07:00
or-else 5087a70d5e merge next 2022-05-12 10:12:50 -07:00
or-else 57466f61b3 updated version to -beta1, updated dependencies 2022-05-12 10:08:05 -07:00
or-else b4a5ad121b bump version to 0.18.6 and rebuild 2022-05-11 15:17:56 -07:00
or-else f9a563248b minor crash in audio and a couple of typos 2022-05-10 21:13:42 -07:00
or-else a4d765da14 merge email link fix 2022-05-10 12:40:48 -07:00
or-else de2f38bbd5 rebuilt with fix 2022-05-10 12:38:45 -07:00
or-else b45451893d fix "contact us" email link 2022-05-10 12:36:54 -07:00
or-else 2c4f005d76 better audio wave colors, move audio config values to config.js 2022-05-07 15:10:35 -07:00
or-else 30d57e11f2 better handling of missing audio preview 2022-05-05 15:50:19 -07:00
aforge d477fd4e82 Merge pull request #94 from tinode/calls-dev
Remove topic.videoCall('accept') error handling.
2022-05-02 11:24:16 -07:00
aforge 06945a01ea Remove topic.videoCall('accept') error handling. 2022-05-01 22:25:44 -07:00
or-else cc75d81538 rebuilt 2022-04-30 10:15:29 -07:00
aforge f50e79ea1e Merge pull request #93 from tinode/calls-dev
Video calls
2022-04-28 22:01:10 -07:00
aforge 706a7e0aaa Remaining code review items. 2022-04-28 22:00:11 -07:00
aforge ed62fe17c5 Some code review changes. 2022-04-27 23:08:19 -07:00
or-else e7290e6c62 bump version to 0.18.5 and rebuild 2022-04-26 09:25:37 -07:00
or-else e938c909a1 merge next 2022-04-25 14:02:35 -07:00
aforge 510a3d7113 Remove trailing spaces. 2022-04-24 22:13:25 -07:00
or-else c36fb2657a rebuilt with the sdk fix 2022-04-24 14:44:49 -07:00
aforge cc66356ad5 Formatting. 2022-04-23 21:29:47 -07:00
aforge c4d4aa77db Video calls: some more cleaup. 2022-04-22 20:43:10 -07:00
aforge 0daef53c87 More UI tweaks (calls).
1. Localize video call messages.
2. Display call message previews correctly.
2022-04-21 21:10:00 -07:00
aforge ad98bec73e UI tweaks.
1. Display call duration.
2. Make mute/unmute buttons work.
2022-04-20 22:33:14 -07:00
aforge 517f457182 Some formatting for video call messages. 2022-04-20 00:08:19 -07:00
aforge 0e92ace6f4 Ringtone for incoming calls. 2022-04-10 22:11:52 -07:00
aforge 3675812353 Merge branch 'devel' into calls-dev 2022-04-07 16:20:22 -07:00
aforge 4b9078e64a Calls logic minor cleanup. 2022-04-06 23:19:09 -07:00
or-else ca8235e1cc rebuilt with the sdk fix 2022-04-05 12:56:52 -07:00
or-else 56b9bb7814 updated dependencies and rebuilt 2022-04-04 17:14:27 -07:00
or-else 78116ef36f updated dependencies and rebuilt 2022-04-04 17:12:03 -07:00
or-else 408b221bac rebuilt 2022-04-04 10:48:01 -07:00
or-else 2a11885ac3 rebuilt with the sdk fix 2022-03-30 19:45:48 -07:00
or-else 62e4a4283f Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=642012 2022-03-27 11:31:54 -07:00
or-else 8fd49cc55e releasing 0.18.3 2022-03-24 14:05:40 -07:00
or-else 60b52b342d rebuilt with the sdk fix 2022-03-23 17:32:08 -07:00
or-else 570ef47455 rebuilt with the sdk fix 2022-03-23 17:14:01 -07:00
or-else ae2d7a12ad rename calcPreviewBars -> resampleBars 2022-03-23 16:10:47 -07:00
aforge b310a94180 Display video call labels. 2022-03-22 00:45:47 -07:00
aforge 45ad8bb2ad Merge branch 'devel' into calls-dev 2022-03-21 22:27:49 -07:00
or-else 2bc89f67f8 rebuilt 2022-03-19 14:53:51 -07:00
or-else 34ba874091 rebuilt with reporting fix 2022-03-19 14:53:27 -07:00
or-else 9ddd103ac5 correctly report topics 2022-03-19 12:56:32 -07:00
or-else 601c048ea5 fix broken spam reporting 2022-03-19 09:18:01 -07:00
or-else 237e97c323 don't immediately subscribe to a new p2p topic, wait for the first message' 2022-03-18 18:58:14 -07:00
or-else 891780bb52 stop recording on topic change 2022-03-17 15:52:25 -07:00
or-else c9391cf451 clear input field when changing topics 2022-03-17 14:12:25 -07:00
or-else 74b756d817 bump version and rebuild 2022-03-17 10:04:17 -07:00
or-else f3def8cf8a fix Drafty.preview signature 2022-03-17 09:48:55 -07:00
or-else 3d2af23f14 fix forwarding previews, remove audio button when forwarding 2022-03-16 20:01:00 -07:00
or-else 7044ce08c1 fix missing reply menu 2022-03-16 16:42:05 -07:00
or-else c5011a4383 fix missing reply menu 2022-03-16 16:34:46 -07:00
or-else d0c4251eda recording+playback+sending works correctly 2022-03-16 16:19:09 -07:00
aforge 2e0ed44763 Clean up video calls ui a bit. 2022-03-15 22:02:00 -07:00
or-else afea2aeec0 crash on missing preview 2022-03-15 19:27:54 -07:00
or-else e121b1645f putting it all together 2022-03-15 18:20:24 -07:00
or-else 2ab56887b7 Merge branch 'devel' into next 2022-03-15 09:14:21 -07:00
or-else 7e3a616983 fix for #91 (typo) 2022-03-15 09:04:14 -07:00
or-else e852e2972a visual cleanup of audio player 2022-03-15 09:03:14 -07:00
aforge ff7f8faf37 Video calls: consolidate onHangup and onClose. 2022-03-14 21:04:29 -07:00
or-else 589b46a8c2 audio messages are fully functional but buggy and ugly 2022-03-14 20:50:24 -07:00
aforge 824f15aa29 Hangup upon topic change. 2022-03-14 00:55:29 -07:00
or-else cc3b1190fd calc audio preview at the source 2022-03-13 15:36:30 -07:00
or-else 025d4cfadb adding audio playback 2022-03-12 18:41:10 -08:00
or-else d3cc0571ce fix date display with deleted messages 2022-03-12 10:53:55 -08:00
or-else 539a37e5ee Unsupported is correctly displayed 2022-03-11 18:39:52 -08:00
or-else e42a7ca6de corrected display of unsupported drafty entities 2022-03-11 16:43:19 -08:00
or-else 609d3e3a9f audio messages are correctly formed and sent 2022-03-11 15:42:07 -08:00
or-else 038f62d535 do not close audiocontext - throws 2022-03-10 18:00:55 -08:00
or-else 37451b9aff clean up audio after finishing recording 2022-03-10 17:42:39 -08:00
or-else 00a1b656e4 audio recorder mostly OK 2022-03-10 17:12:09 -08:00
or-else e0c412a33c audio recording widget is reasonably functional 2022-03-09 16:54:20 -08:00
or-else a2ca095c24 adding support for voice messages 2022-03-08 18:23:13 -08:00
or-else 42ae953055 minor cleanup, support for delayed topic creation 2022-03-07 19:41:17 -08:00
or-else 9c2d3f3693 clean up pushes 2022-03-05 20:09:02 -08:00
or-else 9d8a1fdfc4 removed debug logging 2022-03-04 13:58:11 -08:00
aforge 713cb4ea03 Merge branch 'devel' into calls-dev 2022-03-03 18:15:14 -08:00
or-else c6a9df9fe9 support for "read" pushes 2022-03-03 17:47:32 -08:00
or-else 470d38e583 merge devel 2022-03-01 18:11:20 -08:00
or-else 65dff7d168 bumped version to 0.18.2 and rebuilt 2022-03-01 12:15:42 -08:00
or-else d2d83a0865 rebuilt with updated sdk, version bumped 2022-02-28 16:04:35 -08:00
or-else aa498a7d1f add comment to update Firebase in service worker 2022-02-28 15:42:32 -08:00
or-else d106f6382a google's FCM API is VERY poorly designed 2022-02-24 17:39:51 -08:00
or-else cdff2ee697 a bit more work on delayed topic removal 2022-02-22 12:54:18 -08:00
or-else d76756c67a race condition when clearing DB 2022-02-21 13:20:46 -08:00
or-else 4edb48fd24 rebuilt with the SDK fixes 2022-02-20 18:20:27 -08:00
or-else 860ae42513 rebuilt with updated sdk 2022-02-20 15:17:29 -08:00
or-else 0598a42164 adding support for delayed removal of deleted topics 2022-02-19 20:58:35 -08:00
or-else 9a811708ba merge with master 2022-02-18 15:32:17 -08:00
or-else bb63cb204b rebuilt with fix 2022-02-18 10:12:01 -08:00
aforge f68057fe87 Merge branch 'devel' into calls-dev 2022-02-18 09:15:54 -08:00
or-else fb45c442a8 crash in message preview on empty topic 2022-02-17 16:07:59 -08:00
aforge acf4f0fa2a Merge branch 'devel' into calls-dev 2022-02-16 10:05:49 -08:00
aforge 6cea3e97db Make call establishment with pub/note/info work. 2022-02-15 18:24:33 -08:00
or-else e5545f5f14 bump version to 0.18.0 2022-02-15 17:20:37 -08:00
or-else 24d75dfbbc minor visual tweaks 2022-02-15 17:18:28 -08:00
aforge 5370b39fb6 Video calls 2022-02-13 19:31:06 -08:00
or-else 84a78ce6b5 added date badges (dividers) in messages view 2022-02-10 14:19:51 -08:00
or-else c8756c3a01 webpack updated to v10 2022-02-10 09:55:37 -08:00
or-else e3cb8a7291 merge conflicts resolved 2022-02-10 09:40:37 -08:00
or-else 3c11cbbdf1 adding vscode config to gitignore 2022-02-10 09:33:44 -08:00
or-else 9657a1e61e bump version to 18.0-rc3 2022-02-07 16:52:15 -08:00
or-else 728b3a3a1b crash on missing topic parameters, ranamed var for clarity 2022-01-28 10:17:14 -08:00
or-else 41bf6f3941 rebuilt with updated dependencies 2022-01-27 15:27:05 -08:00
or-else a02be831f2 minor fixes around remote avatars 2022-01-27 15:17:42 -08:00
or-else 5cfcd5789f removed unused imports, stripped src from IM before sending 2022-01-15 20:35:43 -08:00
or-else 28c34bcf7d rebuilt with the sdk fix 2022-01-12 19:15:38 -08:00
or-else e87a36421c fix crash on image with no data 2022-01-12 17:27:39 -08:00
or-else 479752036d rebuilt with the sdk fix 2021-12-29 17:38:43 -08:00
or-else c2d95725e4 fix missing context 2021-12-21 11:04:18 -08:00
or-else 9acca449e3 everything but images seems to be correct 2021-12-20 13:50:14 -08:00
or-else bd2def7882 formatting of forwarded messages previews 2021-12-20 12:27:02 -08:00
or-else b201f019ef reply formatting works correctly 2021-12-19 17:42:19 -08:00
or-else 1d59d29519 use new method for replies 2021-12-19 11:51:24 -08:00
or-else 9582a9f2cd simplify a bit and remove debug logging 2021-12-19 10:59:08 -08:00
or-else 363104755e completely remove quotes in previews 2021-12-19 10:51:16 -08:00
or-else ab7160170d refactoring split formatters - transformers 2021-12-19 10:05:16 -08:00
or-else afcb8d8616 some formatting fixes 2021-12-11 21:49:03 -08:00
or-else 69cf5817f0 rebuilt with the SDK fix 2021-12-05 20:18:25 -08:00
or-else 572fc66a69 sdk fix for attachment rendering in previews, bumped version 2021-12-02 12:00:09 -08:00
or-else 6a877620e6 fix for #90 2021-11-30 18:13:48 -08:00
or-else c7a60b9608 visual tweaks of forwarded replies 2021-11-25 14:57:08 -08:00
or-else 3feca32d13 strip mentioned name from the forwarded preview 2021-11-24 18:55:44 -08:00
or-else 081eb8723d bump version to 0.18.0-rc1 and rebuild 2021-11-21 13:29:08 -08:00
or-else a1034654a9 added [go to latest message] button 2021-11-21 11:06:21 -08:00
or-else faa3f60b55 adding [go to the latest message] button 2021-11-15 13:10:31 -08:00
or-else 31345eb298 rebuilt 2021-11-14 16:24:05 -08:00
or-else a9332495cf bump version to -beta2 and rebuild 2021-11-14 16:23:34 -08:00
or-else e4c5da3178 roll back to firebase 8, otherwise crash 2021-11-14 16:22:13 -08:00
or-else f3c6581fa5 rebuilt 2021-11-10 14:46:44 -08:00
or-else d6cf419266 emphesize that this is not a standaline app 2021-11-10 14:15:49 -08:00
or-else 939804eae0 move push setup to a separate file 2021-11-10 14:11:53 -08:00
or-else 2fc8826403 further shorten forwarded preview 2021-11-09 13:45:50 -08:00
or-else 298bca4042 replace screenshot with a more informative one 2021-11-09 10:58:30 -08:00
or-else f5d3b3af0b rebuilt with the sdk 2021-11-09 10:31:34 -08:00
or-else 733295f7e6 merge next into devel 2021-11-09 10:28:54 -08:00
or-else a706729f1b bump version to -beta1, update dependencies, add missing translation 2021-11-09 10:25:39 -08:00
or-else e180f526fb Use correct placeholder string in [Forward to] contacts search 2021-11-09 10:19:07 -08:00
or-else 990f929b4e shorten perview of a forwarded message 2021-11-09 09:58:25 -08:00
or-else 07292dffa4 dependencies updated 2021-11-09 09:57:43 -08:00
or-else 63b13f8cdd dependencies updated, version bumped to -beta1 2021-11-09 09:57:04 -08:00
or-else 70c15653a9 visual tweaks for mobile view and breakage in send message panel 2021-11-09 09:55:55 -08:00
or-else caf381496f visual tweaks for mobile view and breakage in send message panel 2021-11-09 09:55:22 -08:00
or-else 00b57cd4db screenshots updated 2021-11-07 16:46:38 -08:00
or-else 2c32c102d4 rebuilt with the sdk fix 2021-11-07 15:43:43 -08:00
or-else 57a6a33122 alignment tweak 2021-11-07 15:38:48 -08:00
or-else 73990fb907 correctly switch from forwarding to replying 2021-10-12 17:23:05 -07:00
or-else e3ae013875 fix failure to upload large avatar 2021-10-10 11:13:38 -07:00
or-else 0f031d2f0d added translation for Reply, Forward 2021-10-07 15:20:30 -07:00
or-else 9d61168cf2 bump asset sizes and rebuild 2021-10-07 11:42:59 -07:00
or-else 3feca6cb41 make mention font slightly lighter 2021-10-07 11:39:53 -07:00
or-else 825aa3fb51 cleanup and other minor UI fixes 2021-10-05 17:20:15 -07:00
or-else fb2b4b337b forwarding from channels and some other minor fixes 2021-10-05 16:42:10 -07:00
or-else 8f672405bc forwarding UI prettier and more usable 2021-10-05 15:56:20 -07:00
or-else a17b3d5b9c forwarding dialog looks and works as expected 2021-10-04 16:52:20 -07:00
or-else 2e45ac6bf9 UI tweaking for message forwarding 2021-10-04 15:44:22 -07:00
aforge ab5f2179ea Fix forward menu's height calculation. 2021-10-02 17:07:39 -07:00
aforge b8d1cd6f70 Merge pull request #88 from tinode/features
Message forwarding
2021-09-28 15:43:15 +03:00
aforge 4ad0baf99a Handling of forwarded messages in previews. 2021-09-28 15:40:42 +03:00
aforge 4f1c9e161a Merge branch 'next' into features 2021-09-28 11:25:18 +03:00
or-else a223d910c1 make LazyImage promise cancellable (otheriwse errors in the log) 2021-09-27 15:56:11 -07:00
or-else 2e589bef0c use fixed version of react-intl. 5.20.12 is broken 2021-09-27 15:54:46 -07:00
or-else 730426041a remove BR after the quoted block as it creates more problems than solves on Android 2021-09-27 12:11:03 -07:00
aforge 338e42dd2a Merge branch 'next' into features 2021-09-27 11:21:14 +03:00
or-else 35e3466a79 rebuilt 2021-09-26 15:22:45 -07:00
or-else a1e3cfb577 shorten image annotation 2021-09-26 15:19:43 -07:00
aforge 360a092584 Visual fixes in ForwardMenu. 2021-09-26 19:30:52 +03:00
or-else a9e356b927 make quote bottom padding bigger 2021-09-26 09:24:11 -07:00
aforge b6abc76f3f More code review suggestions. 2021-09-25 20:33:26 +03:00
aforge b1589a8790 Merge branch 'next' into features 2021-09-25 14:41:21 +03:00
or-else 03a2168761 rebuilt 2021-09-24 18:46:29 -07:00
aforge 3989d8cee9 Merge branch 'next' into features 2021-09-24 09:44:34 +03:00
or-else eae565edfa updated dependencies 2021-09-23 14:45:21 -07:00
or-else f9bf9b07d9 restore BR after the quoted text (but hide it using css) otherwise trouble on Android 2021-09-23 10:50:47 -07:00
aforge fb3fae8c9b Replace "Forwarded from" with an icon. 2021-09-23 20:07:21 +03:00
aforge 3a7a806420 Basic message forwarding. 2021-09-21 17:39:24 +03:00
aforge 03f076a1bc Merge branch 'next' into features 2021-09-20 16:56:40 +03:00
Gene 3a4d1f03c0 Merge pull request #87 from tinode/next-2
More refactoring
2021-09-19 20:44:04 +03:00
or-else 96c64b4b27 fix disappearing reply preview in image and doc previews 2021-09-19 10:41:00 -07:00
or-else 4175cffa4d everything seems to work as expected 2021-09-18 15:02:27 -07:00
aforge 9e8b55391a Merge branch 'next' into features 2021-09-18 16:26:54 +03:00
or-else c2c3b743b3 refactoring formatters for lazy display of scaled image previews (WIP) 2021-09-17 22:10:41 -07:00
or-else 8de413c5b5 refactored blob helpers to use promises instead of callbacks 2021-09-17 12:38:35 -07:00
aforge 4791f3dcc3 Merge branch 'next' into features 2021-09-17 09:18:58 +03:00
or-else 760289c6cf Merge branch 'next' of https://github.com/tinode/webapp into next 2021-09-16 18:06:20 -07:00
or-else ae528ef8d6 readme updated: replying is done, forwarding is not 2021-09-16 18:05:24 -07:00
Gene 3e5e9932d8 Merge pull request #86 from tinode/next-2
Mostly visual changes
2021-09-17 03:08:01 +03:00
or-else e72dc20192 everything works 2021-09-16 17:07:05 -07:00
or-else 707dfcf579 update formatjs extraction path 2021-09-16 11:58:49 -07:00
or-else 6343d3db81 fix lost API key for uploaded images 2021-09-16 11:32:03 -07:00
or-else 7117955126 add language attribute to <html> 2021-09-16 11:31:27 -07:00
aforge e98c7d5ed7 Merge branch 'next' into features 2021-09-16 16:20:26 +03:00
or-else 7bcfd974cb fix flashing of the bubble tip 2021-09-15 20:08:27 -07:00
or-else f51ba69fa7 everything seems to work now 2021-09-15 17:57:34 -07:00
or-else 228cc9669c code rearrangement 2021-09-15 13:45:50 -07:00
or-else 3a46599e79 code review changes, attempt at FF fix 2021-09-15 11:16:10 -07:00
or-else 84df30a767 rebuilt 2021-09-14 21:42:48 -07:00
or-else 58ae1359fa this transition is no longer needed 2021-09-14 21:23:33 -07:00
or-else 3d55ae1a89 visual tweaks + unneeded styles removed 2021-09-14 21:06:40 -07:00
or-else c00c1002e3 evrithing seems to work now 2021-09-14 20:36:57 -07:00
or-else 8ee83a9b26 file with formatters 2021-09-14 09:56:24 -07:00
or-else 68e1287fb8 image reply & invalid content fails, otherwise OK 2021-09-13 19:44:43 -07:00
or-else f57c5736e6 fixes 2021-09-12 12:23:14 -07:00
or-else 531a7ed74a a few more visual tweaks 2021-09-09 18:11:02 -07:00
or-else 4746b18ffd playing with colors and padding 2021-09-09 17:54:34 -07:00
or-else 067eba01fc visual update of replies 2021-09-09 16:57:31 -07:00
or-else 80b7a33e9f rebuilt 2021-09-09 11:36:27 -07:00
aforge 4a938b9aa8 Merge pull request #84 from tinode/next-reply
Reply feature draft vs "next"
2021-09-09 19:14:36 +03:00
aforge 55bfc5d2b6 Update umd and css. 2021-09-09 19:13:46 +03:00
aforge dd81190ff3 Code review suggestions.
* Reply to broken images.
* Remove 'viewportWidth' from SendMessage.
2021-09-06 11:26:19 +03:00
aforge 3e4ef8347f Rendering changes. 2021-09-05 21:15:06 +03:00
aforge b833844d09 Use Drafty.preview instead of Drafty.replyPreview. 2021-09-04 19:46:14 +03:00
aforge 2ace6b8ad7 Add a space after icon in IC formatting. 2021-09-01 17:50:14 +03:00
aforge 2fc1b97124 Bug fixes. 2021-09-01 17:45:59 +03:00
aforge 21d3115346 Code review suggestions. 2021-09-01 17:07:18 +03:00
aforge c408cc12c8 Merge branch 'next' into next-reply 2021-09-01 11:51:27 +03:00
or-else 0fd3fc2e23 suppress link on MN in preview, check for invalid content in preview 2021-08-31 21:23:56 -07:00
or-else ffca6573df fix crash on unknown fmt in drafty message (QQ in this case) 2021-08-31 19:44:53 -07:00
aforge 9e7c12f9e5 Minor style change. 2021-08-31 17:23:15 +03:00
aforge df39bccf78 More bug fixes. 2021-08-31 17:08:22 +03:00
aforge 186d9006fc Merge branch 'next' into next-reply 2021-08-31 15:44:35 +03:00
or-else 25bd8d2ada make some checkboxes for long-running state changes 3-state 2021-08-30 16:44:44 -07:00
aforge 2edfaeecde Make handleQuoteClick work. 2021-08-30 23:35:13 +03:00
or-else 331c17a7cc merge next into next-reply 2021-08-29 11:10:13 -07:00
aforge 53936f11e0 Updates to replyTo feature. 2021-08-29 00:40:23 +03:00
aforge 9660b9016b Merge branch 'next-reply' of https://github.com/tinode/webapp into next-reply 2021-08-28 23:45:23 +03:00
or-else 87c73869af releasing v0.17.10 2021-08-26 18:19:52 -07:00
or-else d706cc4f6b a couple of minor changes for clarity 2021-08-26 18:04:23 -07:00
or-else a45843fd39 a bit of refactoring to allow delayed new p2p subscriptions (not done yet) 2021-08-25 14:23:41 -07:00
aforge 656cfb3932 Merge branch 'devel' into features 2021-08-24 09:47:06 +03:00
or-else 0f186b6b3b a bunch of small bugs fixed 2021-08-23 21:10:04 -07:00
or-else efc0dfddc0 fix message scrolling on initial load 2021-08-23 17:16:59 -07:00
or-else 667d0e6d99 fix a race condition in handleScrollEvent (#85) 2021-08-23 17:14:21 -07:00
or-else 763a525292 fix message scrolling on initial load 2021-08-23 16:54:52 -07:00
or-else f3e54e46b5 fix a race condition in handleScrollEvent (#85) 2021-08-23 15:30:08 -07:00
aforge 292e974d29 Merge branch 'devel' into features 2021-08-23 10:41:19 +03:00
or-else 8294248e2d stop typing indicator on message received 2021-08-22 19:40:08 -07:00
or-else e938b4ecd5 rebuilt 2021-08-22 19:11:11 -07:00
or-else bf19cd1351 minor visual fixes 2021-08-22 19:09:29 -07:00
or-else ae197dac8f translation for Archived: 2021-08-22 17:37:46 -07:00
or-else 5ea4fdf05e updated translations: zh, zh-TW 2021-08-22 15:21:33 -07:00
or-else 33eed09c84 translations updated: ko, ro 2021-08-22 15:00:06 -07:00
or-else dd79587a63 updated translations: de, es, ru 2021-08-22 14:39:24 -07:00
or-else e73a920154 rebuilt with translations 2021-08-22 12:23:29 -07:00
or-else 9c4db41b76 fixing unarchive menu, translations 2021-08-22 12:22:47 -07:00
or-else 7ba20c78b1 adding unarchive menu item 2021-08-22 11:45:50 -07:00
or-else a6c29e3e5d rebuilt 2021-08-22 11:01:16 -07:00
or-else 775915e6b2 merging devel 2021-08-22 10:54:32 -07:00
or-else 1d0c7e1c73 fixing "archive" menu item 2021-08-22 10:52:21 -07:00
aforge 9c0bdb4848 Merge branch 'devel' into features 2021-08-22 14:43:24 +03:00
or-else 13432292cc fix vertical text alignment in info-box 2021-08-21 20:24:14 -07:00
or-else 62d289a270 another little fix 2021-08-20 21:05:24 -07:00
or-else b893f28145 a big more of manual merging 2021-08-20 21:02:39 -07:00
or-else ea816859b6 manual merge of devel changes 2021-08-20 20:55:51 -07:00
or-else 9508797ab9 added a "password reset email has been sent" screen 2021-08-20 20:50:08 -07:00
or-else 36aa610b8e tweaking color 2021-08-20 20:39:24 -07:00
or-else 8822c2892e added a "password reset email has been sent" screen 2021-08-20 20:32:56 -07:00
aforge 9626995b1c Merge branch 'devel' into features 2021-08-20 09:25:15 +03:00
or-else ddfbfdcbb3 releasing 0.17.9 2021-08-19 18:56:36 -07:00
or-else b2a02fb78c merge remote 2021-08-19 16:23:31 -07:00
or-else 29e3bd4f9f merge next 2021-08-19 16:22:33 -07:00
or-else c522b90150 a bunch of small fixes in refactored UI 2021-08-19 16:21:16 -07:00
aforge 454b594b96 Merge branch 'devel' into features 2021-08-19 11:44:53 +03:00
or-else 2cc2258682 fixed bugs in tags editing 2021-08-18 19:51:19 -07:00
or-else 529486feb3 colorize user names under messages 2021-08-18 16:00:47 -07:00
or-else faf644d9dd bump version to v0.17.9-rc2 2021-08-17 10:44:38 -07:00
or-else fb3de109e0 merge devel 2021-08-16 16:06:32 -07:00
or-else c34ba46563 rebuilt 2021-08-16 16:05:27 -07:00
or-else b783ec2949 remove loading... spinner on database fetch 2021-08-16 16:04:32 -07:00
or-else 982b40d6e8 mereg devel 2021-08-16 11:52:41 -07:00
or-else 0840a79024 use topic.messages directly instead of creating a copy in this.state 2021-08-16 11:40:30 -07:00
or-else 93548d644f rebuilt 2021-08-15 15:27:27 -07:00
Gene ce07c81bf7 Merge branch 'next' into next-reply 2021-08-15 11:13:44 -07:00
or-else 34afdcac1a rebuilt 2021-08-15 10:54:51 -07:00
aforge 8af1727c3d Merge branch 'devel' into features 2021-08-15 09:47:38 +03:00
aforge 08c295b704 Merge branch 'next' into next-reply 2021-08-15 09:25:32 +03:00
or-else 7eb3ca32bb clear indexeddb on filed token login 2021-08-14 17:08:31 -07:00
or-else d22b35aacc a couple more visual and logical fixes 2021-08-14 16:24:45 -07:00
aforge e065d71aed Merge branch 'next' into next-reply 2021-08-14 16:31:09 +03:00
aforge b482f6649d Basic and very raw implementation of the Reply To feature. 2021-08-14 16:30:18 +03:00
or-else d6ca3f9f3c fix editing permissions 2021-08-13 20:35:39 -07:00
or-else d741f6ec92 merge devel 2021-08-13 17:29:42 -07:00
or-else 4399428fdf rebuilt with translations 2021-08-13 15:08:42 -07:00
or-else e0e25fc6d3 updated dependencies 2021-08-13 15:01:42 -07:00
or-else 25e9c1a885 added missing translations 2021-08-13 15:01:17 -07:00
or-else b59fd06381 fix some visual breakage with long strings in messages 2021-08-13 15:00:38 -07:00
or-else b540ac44df make owner delete chat instead of leaving 2021-08-13 14:59:49 -07:00
aforge 1e08211d17 Merge branch 'devel' into features 2021-08-13 09:49:39 +03:00
or-else 6c739d9c43 bump version and rebuild 2021-08-11 11:53:49 -07:00
or-else 9a2e0519d3 fix deleting the photo 2021-08-10 18:13:21 -07:00
or-else 47e7632399 better caching of out-of-band images 2021-08-09 16:04:31 -07:00
or-else 58e319b58e kind of works, no crashes, but inefficient and not everything is refreshed 2021-08-09 14:03:33 -07:00
or-else 17a658e373 a couple minor tweaks 2021-08-08 18:59:34 -07:00
or-else 1c0b464bed more or less complete 2021-08-08 18:51:25 -07:00
or-else fcb61dcc3b a ton more refactoring 2021-08-07 15:34:22 -07:00
or-else 6d4ff9d625 a lot of refactoring of topic-info 2021-08-06 14:58:05 -07:00
or-else 642821db9f refactoring for better topic and account editing 2021-08-05 16:32:13 -07:00
or-else dafc3161c5 fixing alignment issues with long user and topic names 2021-08-04 21:46:59 -07:00
aforge 34712ba1f0 Merge branch 'devel' into features 2021-08-04 09:47:58 +03:00
or-else 47304c7baf merge master 2021-08-03 20:19:35 -07:00
or-else 37e8912fa5 merge devel 2021-08-03 20:15:53 -07:00
or-else 274da95b71 ignore 2021-08-03 20:09:40 -07:00
or-else 59138c9f5f fix i18n string extraction 2021-08-03 18:52:42 -07:00
or-else b555595f64 rebuilt 2021-08-03 16:36:22 -07:00
or-else b2daf6b58e bump version and rebuild 2021-08-03 11:18:48 -07:00
or-else e636c4a30b rebuilt 2021-08-03 11:14:21 -07:00
aforge 4005b01a85 Merge branch 'devel' into features 2021-08-03 16:27:10 +03:00
or-else 063245e665 refreshed Translations section 2021-07-31 15:02:42 -07:00
or-else e3bdd5bb45 zh-TW translations in service worker 2021-07-30 09:40:10 -07:00
or-else 122fd018d0 fixing #80 in service-worker 2021-07-30 09:37:25 -07:00
or-else febc75da17 ensure sort order of contacts by [touched] 2021-07-29 19:16:38 -07:00
or-else eff8bbabc5 bump version and rebuild 2021-07-29 17:12:09 -07:00
or-else c7f4fa0165 rebuilt with chinese traditional translation 2021-07-29 17:11:52 -07:00
or-else a0312f8b52 fix leaving a conversation 2021-07-29 17:11:08 -07:00
or-else 43be8d6662 reguilt with the next translation 2021-07-29 14:18:47 -07:00
Gene 8570677388 Merge pull request #82 from SiderealArt/patch-1
Update zh-TW.json
2021-07-27 17:36:04 -07:00
SiderealArt 081548b35a Update zh-TW.json 2021-07-28 08:05:54 +08:00
or-else fd14638c83 update format of zh-TW translation file 2021-07-27 11:29:34 -07:00
or-else 87fa1ee107 Merge branch 'devel' into next 2021-07-27 10:11:44 -07:00
or-else f8879f7b68 rename the new translation according to ISO 639-1 2021-07-27 10:11:25 -07:00
Gene 39c8b9a4a1 Merge pull request #81 from SiderealArt/patch-1
Add Traditional Chinese translation
2021-07-27 09:45:29 -07:00
SiderealArt 812cdd970c Add Traditional Chinese translation 2021-07-27 19:22:34 +08:00
or-else 3da0984ae0 clarifying a couple of comments 2021-07-22 15:33:33 -07:00
aforge 1961e76699 Basic menu item for message replies. 2021-07-20 18:19:34 +03:00
or-else 62fc84912e refactored translations to avoid breakage 2021-07-18 21:54:39 -07:00
or-else eb299b73d2 converted translations to a new format to avoid data loss on failed builds 2021-07-18 21:31:13 -07:00
or-else b35ff16faa rebuilt 2021-07-18 19:27:11 -07:00
or-else 094173fe02 refactoring info-view into reusable components 2021-07-18 19:26:34 -07:00
or-else fdfede5069 merge devel 2021-07-17 11:10:34 -07:00
or-else a24ee01b37 remove crash in Safari, #80 2021-07-17 10:28:35 -07:00
or-else 92f1d53e48 correct use of reference 2021-07-16 14:28:13 -07:00
or-else f81aaea837 change method signatures to make TopicCommonView usable in INfoView 2021-07-16 14:19:56 -07:00
or-else f1ba810765 remove deprecated findDOMNode 2021-07-16 14:18:38 -07:00
or-else b704499308 making AccGeneralView more generic, using msg.extra for attachments 2021-07-16 12:57:30 -07:00
or-else 0cd432a52f minor cleanup 2021-07-15 18:51:56 -07:00
or-else 0afef6c587 fixing display of avatars by URL 2021-07-15 17:29:06 -07:00
or-else 4219e47529 making uploads work 2021-07-14 16:06:37 -07:00
or-else 8320bf28ce looks like cropping works correctly 2021-07-13 16:27:51 -07:00
or-else ba5b82115f fixxing some build breakage 2021-07-13 09:49:20 -07:00
or-else bb9d47c7e0 adding new view name 2021-07-13 09:36:40 -07:00
or-else f1196e17ff wip: cropping mostly works, uploads do not yet 2021-07-13 09:35:17 -07:00
or-else ab962bfe7d wip: image cropping 2021-07-13 09:33:49 -07:00
or-else 07854ba9fc wip on avatar cropping 2021-07-10 19:22:56 -07:00
or-else f2cd1ba0f2 making avatar cropping work 2021-07-10 19:11:21 -07:00
or-else 6b8595750a merge devel 2021-07-10 17:26:20 -07:00
or-else aa451946a5 releasing 0.17.6 2021-07-10 17:25:00 -07:00
or-else 0195aae6ff wip: hooking up avatar cropper 2021-07-10 17:17:48 -07:00
or-else 6b8c3949a0 add src/views/avatar-crop-view.jsx 2021-07-10 15:39:56 -07:00
or-else bc6e2ab8d2 wip: avatar panning and zooming 2021-07-10 15:39:02 -07:00
or-else 72f4f47e8f adding zoom/crop widget for avatar uploads 2021-07-10 12:40:18 -07:00
or-else ecb5729227 rebuilt with changes 2021-07-07 16:50:22 -07:00
or-else fd8e2d9a17 work in progress on avatar uploading 2021-07-07 16:49:44 -07:00
or-else 4975084eef merge conflic resolved 2021-07-07 16:39:03 -07:00
or-else d1ea1d7658 add React.StrictMode checking for devel 2021-07-07 16:35:39 -07:00
or-else 626484d8cb releasing 0.17.6-rc1 2021-07-07 15:24:12 -07:00
or-else 864753942f rebuilt with indexedDB initialization fix 2021-07-06 09:33:21 -07:00
or-else 66c2db86d9 rebuilt with the fix & bumped versionfor new release 2021-07-02 10:47:05 -07:00
or-else e30868c072 wip: out of band avatars 2021-07-01 14:19:28 -07:00
or-else c5c08aa4f9 minor visual fixes 2021-06-28 10:45:16 -07:00
or-else bc67378bc9 adding missing korean translation 2021-06-28 09:44:56 -07:00
or-else 649d6e0494 trusted badges work as expected 2021-06-27 18:20:45 -07:00
or-else 1a8f6351ef rebuilt 2021-06-26 08:59:12 -07:00
or-else 58426cd0ee adding support for trusted, verified/staff/danger badges, wip 2021-06-25 12:27:03 -07:00
or-else 3d0d7048aa start 0.18.0 development 2021-06-25 11:17:18 -07:00
or-else 9033c1ade0 add menu item for restoring topics from archive 2021-06-25 11:17:01 -07:00
or-else bf9ba90ed3 clear reconnect timer on logout 2021-06-24 19:54:58 -07:00
or-else 07aa38e162 rebuilt with sdk fixes 2021-06-23 17:32:38 -07:00
or-else 181161250a rebuilt with the sdk fixes 2021-06-23 15:57:24 -07:00
or-else 019b270242 wrong order of params in file uploads 2021-06-23 14:03:41 -07:00
or-else cf5fd9753c release version 0.17.2 and rebuild 2021-06-21 14:47:53 -07:00
or-else a465fcf935 update dependencies, bump version and rebuild 2021-06-20 10:47:27 -07:00
or-else 2d01e0af85 fixed topic subscription on page reload 2021-06-18 19:13:59 -07:00
or-else 3fcd118af8 bumped version and rebuilt with fixes 2021-06-18 17:58:56 -07:00
or-else cdf27b4d48 fixed layout of tabs in new topic view 2021-06-18 17:58:32 -07:00
or-else ab993e36b9 bumped version, updated dependencies, rebuilt 2021-06-15 14:53:08 -07:00
or-else 9277db4dcd rebuild 2021-06-10 19:55:42 -07:00
or-else 9b76577bb9 fix occasional breakage in makeImageDataUrl 2021-06-10 19:54:42 -07:00
or-else c57fd6e539 fix DB initialization and teardown, wrong upload parameters order 2021-06-09 15:58:07 -07:00
or-else 2ffe42741d merge devel 2021-06-04 18:23:41 -07:00
or-else 5d97746d9d remove support for obscure browsers 2021-06-04 18:21:51 -07:00
or-else e22a84c8ad Merge branch 'master' of https://github.com/tinode/webapp 2021-06-03 18:20:58 -07:00
or-else 2b12baa166 updated dependencies, bumped version to 0.17.1 and rebuilt 2021-06-03 18:20:12 -07:00
or-else 2ecb522b44 updated dependencies including react-intl (previously broken) 2021-06-02 17:43:43 -07:00
Gene 6527ae8613 Update feature_request.md 2021-06-02 00:26:30 +03:00
Gene 10aa29e68b Update issue templates 2021-06-02 00:25:59 +03:00
or-else d944ddccd5 bump version to 0.17.1-rc and rebuild 2021-05-30 11:33:33 -07:00
or-else ef469ce419 rebuilt 2021-05-19 15:19:06 -07:00
or-else c1b3040f8a ad references to native apps to manifest 2021-05-19 15:13:08 -07:00
or-else bf0c818c28 pass topic name in form response 2021-05-19 15:13:05 -07:00
or-else 92f7464477 ignore trailing deleted ranges for message previews 2021-05-19 15:11:06 -07:00
or-else 034944877c fix cache issue on logout 2021-05-19 13:44:24 -07:00
or-else 694b93ebe8 merge devel 2021-05-10 17:00:00 -07:00
or-else 29e5be4e01 pass topic name in form response 2021-05-10 16:50:24 -07:00
or-else 02e23c885a tabs to spaces, typo in comment 2021-05-10 11:40:58 -07:00
or-else 74d5473a8b adding support for out of band avatars 2021-05-10 11:38:53 -07:00
or-else 02e1a28e89 rename vcard to theCard 2021-05-09 14:12:46 -07:00
or-else 2c61ab26c8 prepare for foreground notifications 2021-05-09 14:07:41 -07:00
or-else eeb14cf752 better cache handling 2021-05-09 14:07:05 -07:00
or-else e97e708c01 adding another generated file 2021-05-08 18:01:47 -07:00
or-else aa8d60cc1d a bit of cleanup, better naming of the app cache 2021-05-08 18:01:14 -07:00
or-else 62259ab331 releasing 0.17.0 2021-05-06 11:04:52 -07:00
or-else c010d1d17a bump version and rebuild 2021-05-03 16:39:46 -07:00
or-else 91e3f23d4e rebuilt with sdk fixes 2021-05-03 16:20:37 -07:00
or-else 1d6b970d7b removed "undefined" color 2021-05-02 16:48:53 -07:00
or-else 430b2db9aa fix bubble being too wide on mobile 2021-05-02 09:48:07 -07:00
or-else 4a480d71cc fix the issue with loading topics from cache 2021-05-01 20:07:24 -07:00
or-else 1537016942 fix incorrect initialization of promise when initializing pushes 2021-05-01 08:14:54 -07:00
or-else 1023e80fa6 another really monor visual tweak for large images 2021-04-30 11:45:34 -07:00
or-else 91e1959833 bump version to -rc2 and rebuild 2021-04-29 13:44:06 -07:00
or-else 0add66f607 a few more visual tweaks 2021-04-29 10:41:03 -07:00
or-else 273e34de2c clear reconnect timer on new connection 2021-04-28 17:05:00 -07:00
or-else 7adc06332e rebuilt 2021-04-28 16:33:24 -07:00
or-else 271fbc4b02 message timestamp is always shown on the bottom right now 2021-04-28 16:32:36 -07:00
or-else f6acbaab21 i18n of drafty preview elements 2021-04-28 13:34:14 -07:00
or-else 8715251e9b explain the workaround 2021-04-26 20:21:35 -07:00
or-else 8c5660173c workaround for what looks like a chrome bug 2021-04-26 19:41:58 -07:00
or-else 52ec124cd6 hopefully last fix 2021-04-25 16:53:40 -07:00
or-else cf423ff5eb formatting fixes 2021-04-25 16:52:30 -07:00
or-else bd24711785 refreshed the screenshots 2021-04-25 16:48:52 -07:00
or-else 304b5fac6e bug fix: don't cache POST responses 2021-04-25 16:48:00 -07:00
or-else 9cfbfe48ea preserve navigation state on page reload 2021-04-23 17:58:33 -07:00
or-else 9e32098b55 later builds of react-intl do not support umd 2021-04-21 17:13:56 -07:00
or-else 2a1814254c dependencies updated 2021-04-20 17:40:28 -07:00
or-else e0633bfa0d bump version and rebuild 2021-04-20 15:41:47 -07:00
or-else a33891a5a9 refresh channels on push, correctly set push enabled status 2021-04-12 16:59:01 -07:00
or-else 8504e4a155 fix FCM initialization and a crash 2021-04-09 16:31:33 -07:00
or-else 390a380763 bump version and rebuild 2021-04-08 15:12:07 -07:00
or-else c01f80c248 more generic names of some css classes 2021-04-06 17:34:10 -07:00
or-else 7427c45c81 rebuilt 2021-04-04 18:37:06 -07:00
or-else 5175ac1dfe make some css class names more generic 2021-04-04 18:35:41 -07:00
or-else 9e17dc73fe rebuilt with linked sdk 2021-04-04 17:33:06 -07:00
or-else 8843e562da possible fix for #67 2021-04-04 11:46:26 -07:00
or-else 5146a6c3d8 rebuilt 2021-03-30 19:40:34 -07:00
or-else c22ee282e9 fix: remove Not Found from author field in deleted group messages 2021-03-29 19:05:19 -07:00
or-else 0b5ac811c4 version bumped, rebuilt 2021-03-25 16:09:48 -07:00
or-else 096399fb0a rebuilt with fixes 2021-03-15 16:48:47 -07:00
or-else e703490a1e add 'chn' to the list of permitted topic name prefixes 2021-03-15 16:47:31 -07:00
or-else 6cd180771a fix missing topic name in contacts 2021-03-15 16:47:03 -07:00
or-else beb861d707 rename ambiguous variable 2021-03-15 16:46:39 -07:00
or-else d4f299aa40 fix broken confirmation menus 2021-03-14 14:04:46 -07:00
or-else c2b1433ed6 cosmetic fixes 2021-03-13 16:56:19 -08:00
or-else 6157557a12 Merge branch 'devel' of https://github.com/tinode/webapp into devel 2021-02-28 15:27:19 -08:00
or-else 0443814f0c add required react keys to extra elements 2021-02-28 15:27:02 -08:00
or-else a5537a5664 fix display of form previews 2021-02-28 14:48:54 -08:00
or-else ebb7537b2f dependencies updated 2021-02-28 14:47:33 -08:00
or-else efd1461102 visual tweaks of message preview and buttons 2021-02-28 14:46:29 -08:00
Gene 7022cf1f64 Merge pull request #64 from kant/patch-3
Typos fixed (lines 51 & 108)
2021-02-22 18:32:21 -08:00
Darío Hereñú 6689e68a7a Typos fixed (lines 51 & 108) 2021-02-22 21:19:58 -03:00
or-else 99423dd04f clear device token when requested 2021-02-21 16:48:27 -08:00
or-else 50344f3cd5 adding placeholder for foreground topic fcm messages 2021-02-17 18:37:06 -08:00
or-else b410879ca9 google renamed a method, fixing 2021-02-17 18:36:17 -08:00
or-else d904af2d2c rebuild with sdk bug fixes 2021-02-13 08:54:49 -08:00
or-else d28d498e03 change version to -alpha1 2021-02-11 14:19:50 -08:00
or-else c81cfa3af3 fix for #62 2021-02-05 11:12:06 -08:00
or-else 705932825a bump version to beta-1 2021-02-04 15:29:22 -08:00
or-else 6ace7e72ab message previews work 2021-02-03 19:04:25 -08:00
or-else 4d972791af build with correct version of the sdk 2021-01-29 09:42:03 -08:00
or-else 3187625dc5 merging devel fixes 2021-01-29 09:38:26 -08:00
or-else fb5447c92f fix misnamed message ids 2021-01-29 09:20:22 -08:00
or-else 55c24f884e Merge branch 'devel' into next 2021-01-28 17:18:40 -08:00
or-else 5795e40573 rename flatten-messages.js -> pack-messages.js 2021-01-28 16:58:11 -08:00
or-else f7652ec3c3 don't minimize dev output 2021-01-28 16:57:39 -08:00
or-else 60b4ed034a rebuilt messages 2021-01-28 16:53:11 -08:00
or-else 189634657e updated scripts and added locale section 2021-01-28 16:52:21 -08:00
or-else dfcacdc392 updated scripts and added locale section 2021-01-28 16:52:03 -08:00
or-else 25a22d7da9 mention new translation scheme 2021-01-28 16:51:11 -08:00
or-else 9e574aa6c5 check in file with raw extracted strings 2021-01-28 16:28:19 -08:00
or-else 3144e7a721 a few more format changes 2021-01-28 16:23:17 -08:00
or-else bc3f13d360 fix message format which causes failure to extract 2021-01-28 16:02:29 -08:00
or-else 010665284b firmatjs requires exlicit declarations everywhere now 2021-01-27 18:21:06 -08:00
or-else 6f47cc50b0 resolving merge conflict 2021-01-27 09:42:25 -08:00
or-else 1172adefb4 fix formatjs setup, #62 2021-01-26 13:58:59 -08:00
or-else 4f38a2d8b1 fixing formatjs build 2021-01-25 17:29:43 -08:00
or-else d75448c548 rebuilt with sdk fixes 2021-01-24 19:03:51 -08:00
or-else 9c69b7129c tinode setup is async now 2021-01-24 17:41:41 -08:00
or-else 4ea82f85ba rebuilt with refactored sdk 2021-01-23 19:08:24 -08:00
or-else 3132294cce persistent cached hooked up on cold start; warm start still crashes 2021-01-22 16:55:01 -08:00
or-else 6f278f5bb5 indexeddb cache kind of works but buggy and incomplete 2021-01-21 20:17:24 -08:00
or-else 92e3f15c01 adding indexedDB persistent cache 2021-01-20 17:45:01 -08:00
or-else 1f5a76e481 received marker in message previews 2021-01-19 18:50:18 -08:00
or-else 4445a90437 fix UI jerk on updating message preview 2021-01-17 18:08:16 -08:00
or-else 0afa8bcdcd more work on supporting message previews 2021-01-17 17:26:57 -08:00
or-else 55d7e0d5cb don't show time in dates long in the past 2021-01-14 13:59:20 -08:00
or-else 4b07c3b5c9 drafty previews 2021-01-13 15:17:56 -08:00
or-else a319e2b0bc CDN changed react-int paths 2021-01-11 19:39:24 -08:00
or-else 924459f84b update build due to changes in webpack and react-intl 2021-01-11 16:21:26 -08:00
or-else 396b23ee88 dependencies updated 2020-12-27 20:55:46 -08:00
or-else 775bc954e3 bump version and rebuild 2020-12-27 10:38:53 -08:00
or-else 6fabb6c111 fix missing file in npm package, remove screenshots from package 2020-12-20 13:11:48 -08:00
or-else e328e6a4c2 dependencies updated 2020-12-20 13:01:55 -08:00
or-else 443245280f rebuild with sdk changes 2020-12-20 12:14:42 -08:00
or-else a248151243 rebuilt with a fix in the sdk 2020-12-18 12:11:07 -08:00
or-else e05e75ed28 use server-provided limits instead of hardcoded ones 2020-12-17 20:14:03 -08:00
or-else 4d450e6b85 show indicators for channels 2020-12-16 16:27:23 -08:00
or-else b014592381 rebuilt with the bug fix in tinode-js 2020-12-13 19:03:16 -08:00
or-else 1d1c9400f8 dependencies updated 2020-12-11 17:19:01 -08:00
or-else 97d4bcfba9 update version, add romanian translation to service worker 2020-12-10 11:07:04 -08:00
or-else ed19ed750c change style of divider in chat list 2020-12-06 20:18:28 -08:00
or-else 93e7b69f88 Merge branch 'devel' of https://github.com/tinode/webapp into devel 2020-12-06 19:47:09 -08:00
or-else af58db914b fixed id of a translated message 2020-12-06 19:47:01 -08:00
or-else 0c9e6379d6 mention Romanian translation, fix reference to error message 2020-12-04 12:00:09 -08:00
Gene 311b36b977 Merge pull request #60 from georgelemon/ro-language-support
Adding i18n support for romanian language
2020-11-24 13:37:14 -08:00
George Lemon d12fd0e16f Adding i18n support for romanian language 2020-11-24 22:45:26 +02:00
or-else a7998a1d29 better visual style of broken images 2020-11-21 17:21:56 -08:00
or-else 231635f86d increase max inline image size 2020-11-10 14:47:58 -08:00
or-else 5574085a01 rebuilt 2020-11-02 15:08:31 -08:00
or-else 5d53899f89 fix for https://github.com/tinode/chat/issues/563 2020-11-02 11:38:41 -08:00
or-else 97c8114fb7 uploading and downloading works 2020-10-25 16:19:56 -07:00
or-else 28d825c681 out of band image uploads work, need testing and polish 2020-10-24 20:49:43 -07:00
or-else cede800726 wip: handling previews/cancellations while uploading 2020-10-22 17:40:40 -07:00
or-else 53c1f9fd0b everyting works except upload progress 2020-10-17 19:05:31 -07:00
or-else 796f16a003 works except for upload progress 2020-10-16 17:43:12 -07:00
or-else 738d84045d wip: fixing bugs in image uploads and display 2020-10-15 17:36:35 -07:00
or-else de1028e141 wip: out of band inline images are starting to work 2020-10-14 16:55:54 -07:00
or-else f9d622a8d8 wip: image scaling by size 2020-10-13 16:30:34 -07:00
or-else fbdf1df9d0 wip: adding out of band image sending 2020-10-13 14:46:45 -07:00
or-else 09baee8d73 Merge branch 'devel' 2020-10-11 10:23:33 -07:00
or-else 84d0ec5e9c bump version 2020-10-11 10:23:06 -07:00
or-else 4d9bc435fc Merge branch 'master' of https://github.com/tinode/webapp 2020-09-30 11:40:16 -07:00
Gene 7d08e65e64 Merge pull request #59 from HitmanCodes/master
Update README.md
2020-09-30 08:12:52 -07:00
HitmanCodes 6e19bf6add Update README.md 2020-09-30 19:16:46 +05:30
or-else 96ad18a902 dependencies updated 2020-09-15 13:57:35 -07:00
or-else 58983588a4 show "channel" instead of "online" in message view caption 2020-09-14 15:54:14 -07:00
or-else 9401a8d483 topic redirects work now, bug with starting topic from search results fixed 2020-09-04 11:01:44 -07:00
or-else 6019303f2c fixes for bugs in starting new topics 2020-09-03 15:03:51 -07:00
or-else 28fd6a7236 fix channel permissions & disable deleting messages 2020-08-25 17:44:14 -07:00
or-else e8d48eb5f0 adding missing dependency 2020-08-25 13:50:48 -07:00
or-else d91534236f dependencies updated 2020-08-24 11:38:01 -07:00
or-else 4d3682c0ef rebuilt 2020-08-01 15:12:20 -07:00
or-else 45c6cf22ff support for channels 2020-08-01 15:10:37 -07:00
or-else c56b30337f rebuilt 2020-07-31 15:40:07 -07:00
or-else ab128ba6bb dependencies updated, security fix for elliptic pkg 2020-07-30 13:51:40 -07:00
or-else b2bbe75f6d bump version. channel UI works 2020-07-20 14:12:20 -07:00
or-else 0f095ceeca adding channel support 2020-07-20 12:20:38 -07:00
or-else 331dea37cf bump version and rebuild 2020-07-17 10:02:40 -07:00
or-else af72df8a4f dependencies updated 2020-07-17 09:22:26 -07:00
or-else 4f56dfc276 hide load spinner on failed login 2020-07-13 18:20:38 -07:00
or-else 2817d83c56 bump version and rebuild 2020-07-12 09:50:45 -07:00
or-else 571b876626 bump version and rebuild 2020-07-06 09:17:32 -07:00
or-else 60867671aa rebuilt with german translation 2020-07-03 15:43:19 -04:00
or-else 1d46599a1f add german strings to service worker 2020-07-01 06:10:08 -04:00
or-else 04734a7dd9 Merge branch 'devel' of https://github.com/tinode/webapp into devel 2020-07-01 06:06:42 -04:00
or-else 37351f9fdf mention German translation 2020-07-01 06:06:25 -04:00
Gene 79fba3a402 Merge pull request #56 from natowi/patch-1
German translation
2020-07-01 12:10:34 +03:00
natowi 7c9cc230ac German translation 2020-06-30 21:19:59 +02:00
Gene ff2f6b035a Update issue templates 2020-06-28 15:43:43 +03:00
or-else b16d538942 another screenshot added 2020-06-23 17:40:12 +03:00
or-else abd94d6858 refreshed screenshots 2020-06-23 17:39:49 +03:00
or-else 6ab2d8663e bump version and rebuild 2020-06-23 10:42:42 +03:00
or-else 2576fa2a87 bump version and rebuild 2020-06-17 14:22:58 +03:00
or-else d0627d5370 visual and translation fixes 2020-06-11 22:03:23 +03:00
or-else a192129ff9 dependencies updated, breaking change in react-intl fixed 2020-06-08 18:20:12 +03:00
or-else ee6b87c3a9 add actual translations 2020-06-03 15:06:21 +03:00
or-else 7fccd41ced spanish translation by @sanmiguel2019 added 2020-06-03 15:03:30 +03:00
or-else b95e3269ea mention Korean translation in README 2020-06-03 11:01:35 +03:00
or-else 38c0023e23 korean translation included into build 2020-06-02 19:02:40 +03:00
Gene 1efc9a90cb Merge pull request #53 from kimtaeyoung-98/master
Korean translation of en.json
2020-06-01 20:11:33 +03:00
kimtaeyoung-98 de04006afc en.json문서의 한국어 번역
Removed spaces.
2020-06-02 01:37:13 +09:00
kimtaeyoung-98 d484bff407 Korean translation of en.json
Korean translation of en.json
2020-06-01 19:04:08 +09:00
or-else 7cd5f786b2 rebuilt with sdk fix: always close the socket 2020-05-24 11:04:48 +03:00
or-else 65c94c9f12 a few more i18n fixes 2020-05-10 14:26:36 +03:00
or-else c3e5b1cabc fix i18n formatting issues 2020-05-10 14:02:52 +03:00
or-else ad97c5638f rebuilt 2020-05-10 12:54:20 +03:00
or-else c9d05642e2 fix issue with scrolling on image preview 2020-05-10 12:53:50 +03:00
or-else 3410128d6f rebuilt 2020-05-09 09:25:47 +03:00
or-else b7a8d4d09d mute in addition to blocking 2020-05-08 11:06:53 +03:00
or-else 919047cfde define and use max tag count 2020-05-01 17:19:18 +03:00
or-else fbaf98c117 use standard inband limit 2020-04-29 15:17:13 +03:00
or-else 5d96331ef5 bump version and rebuild 2020-04-26 11:20:03 +03:00
or-else 587d457712 bump version and rebuild 2020-04-22 11:50:56 +03:00
or-else 979b283928 fix for hidden group-manager control panel 2020-04-21 21:16:54 +03:00
or-else 7c70dea398 bump version and rebuild 2020-04-20 12:01:26 +03:00
or-else 3f04d0e23b more localizations 2020-04-18 12:26:45 +03:00
or-else d953f6ac3e bump version and rebuild 2020-04-17 14:36:44 +03:00
or-else e5e78755ae blocked chats display 2020-04-17 14:30:26 +03:00
or-else 52c1016b45 fix disappearing [you] badge in topic members 2020-04-15 15:55:40 +03:00
or-else c0a3ba6a92 make topic and user deletions "hard" 2020-04-15 09:45:49 +03:00
or-else 6be7b87173 small refactoring of enabled permissions in topic 2020-04-15 09:45:15 +03:00
or-else 35a63ff5ab fix alert dialog 2020-04-13 19:00:38 +03:00
or-else 375750a6ff bump version and rebuild 2020-04-13 16:13:34 +03:00
or-else ea8ac3c293 refactoring of account settings into multiple screens 2020-04-13 16:01:24 +03:00
or-else 5507ccd728 refactoring account settings into multiple screens 2020-04-13 10:35:33 +03:00
or-else b96c8c2516 bump version and rebuild 2020-04-03 14:22:36 +03:00
or-else e04240870d rebuilt 2020-04-02 10:15:54 +03:00
or-else 506cd00b27 chrome bug was fixed 2020-04-02 08:18:38 +03:00
or-else cdd40facec support for incognito mode 2020-04-01 21:13:32 +03:00
or-else 6222610836 rebuilt with different background 2020-03-26 14:27:24 +03:00
or-else 16b5bdf37d Merge branch 'devel' of https://github.com/tinode/webapp into devel 2020-03-20 15:08:23 +03:00
or-else 6e39fe9bef use different background image for messages panel 2020-03-20 15:08:11 +03:00
Gene 78c2f1aaa1 Merge pull request #46 from mzch/master
Fix 2 bugs in Linux environment
2020-03-14 18:25:37 +03:00
Koichi MATSUMOTO 947b166935 move postcss-cli to devDependencies
move postcss-cli to devDependencies
2020-03-14 22:20:15 +09:00
Koichi MATSUMOTO 337afcf075 fix lack of postcss-cli
fix lack of postcss-cli
2020-03-14 20:08:04 +09:00
Koichi MATSUMOTO 77e67bb714 fix typo
fix typo
2020-03-14 20:02:40 +09:00
or-else dbcac419de replaced UglifyJsPlugin with TerserPlugin 2020-03-14 09:39:52 +03:00
or-else 475e5eb3e7 dependencies updated 2020-03-14 09:09:27 +03:00
or-else 12f05e8570 bump version and rebuild 2020-03-08 16:28:45 +03:00
or-else 527a01bebb critical bug: login stalls after refresh 2020-02-27 19:54:32 +03:00
or-else ffe2ab7737 bump version and rebuild 2020-02-25 09:30:06 +03:00
or-else 2fd8346dc5 check Drafty for validity 2020-02-23 21:44:08 +03:00
or-else fd4cb58f0c more prominent "messaging disabled" note 2020-02-23 20:46:22 +03:00
or-else d2647d5f0c fix: incorrect redirect on failed sign up 2020-02-23 13:26:24 +03:00
or-else e27c106c33 pass himan UI language to tinode-sdk 2020-02-23 10:57:04 +03:00
or-else e7840b05a6 removed debug printing 2020-02-18 14:34:40 +03:00
or-else 3484c6549e rebuilt with SDK fix 2020-02-18 14:01:35 +03:00
or-else 30054aec04 formatting 2020-02-14 09:59:35 +03:00
or-else fae274062a correct link to web-app icon 2020-02-14 09:58:35 +03:00
or-else 3b77af2a10 adding missing web-app.svg icon 2020-02-14 09:58:03 +03:00
or-else a4b757fa95 bump version and rebuild 2020-02-08 12:23:51 +03:00
or-else 9d7e1d4f63 rebuilt with fixed note-read & translations 2020-02-05 10:18:59 +03:00
or-else 858e62f732 google translate missing chinese strings 2020-02-04 18:05:58 +03:00
or-else 50a775fc72 fix missing read notifications 2020-01-22 21:01:05 +03:00
or-else feaefdb952 update and restructure for clarity 2020-01-22 11:05:02 +03:00
or-else 755ad00619 merge next 2020-01-21 22:09:45 +03:00
or-else c6e440ca72 merge 2020-01-21 16:18:19 +03:00
or-else f34f786bc6 bump version and rebuild 2020-01-21 14:24:21 +03:00
aforge b2e7a4d63c Merge pull request #43 from tinode/retry-context-menu
Add Retry option in context menu for failed messages.
2020-01-20 22:35:58 -08:00
aforge f100385c79 Minor syntax change. 2020-01-20 22:34:39 -08:00
aforge 0081ed3716 Add Retry option in context menu for failed messages. 2020-01-20 21:25:22 -08:00
or-else 065115d2bf done 2020-01-18 13:42:19 +03:00
or-else ed6518df9c almost works 2020-01-18 11:36:57 +03:00
or-else f7a52d70d6 first pass at file attachment previews 2020-01-18 11:15:37 +03:00
or-else 72d978ed55 previewing image before sending works now 2020-01-14 18:07:56 +03:00
or-else 90889454a0 fix placeholder text positioning 2020-01-13 19:25:07 +03:00
or-else d1c90f8434 more refactoring of image preview 2020-01-13 18:20:09 +03:00
or-else 8622926a4c first pass at implementing image preview before sending 2020-01-13 10:20:12 +03:00
or-else 2de35424b5 updated FCM instructions 2020-01-13 10:19:36 +03:00
or-else d603953a77 bump version and rebuild 2020-01-11 14:21:16 +03:00
or-else d0e3e238b8 missing "content deleted" when all content is deleted 2020-01-10 12:25:46 +03:00
or-else 83dfb5e9b2 fix for incorrect "Not found" user name in messages 2020-01-09 20:54:25 +03:00
or-else 727d926bf6 dependencies updated 2020-01-09 20:53:45 +03:00
or-else fe9f4bb17e bump version and rebuild 2020-01-05 16:34:26 -05:00
or-else 1afd876d99 updated instructions for setting up firebase in web app 2019-12-30 15:42:41 -05:00
or-else 6fee1ce5ea firebase made a breaking change in 7.0.0 - need different config 2019-12-30 15:31:39 -05:00
or-else 7828c372e3 better exclusion of node modules cache 2019-12-29 20:48:17 -05:00
or-else e02047345c check in .gitignore 2019-12-29 20:46:56 -05:00
or-else db76ec3266 dependencies updated 2019-12-23 10:27:34 +03:00
or-else 06de1c1fb6 bump version and rebuild 2019-12-23 09:54:35 +03:00
or-else a662787b9c fix for different representation of deleted messages 2019-12-12 15:58:21 +03:00
or-else bc4c0811d1 indicators of deleted content work now 2019-12-06 10:41:21 +03:00
or-else 893cbdaa0c does not crash on deleted messages 2019-12-03 13:01:23 +03:00
or-else 44c5e30f4d all dependencies updated 2019-11-30 12:16:29 +03:00
or-else b81ec3e634 update dependencies and bump version 2019-11-30 11:21:10 +03:00
or-else b48e4e63d9 rebuilt 2019-11-20 11:49:06 +03:00
or-else dce1875de4 simplify handling of new message notifications 2019-11-20 11:44:15 +03:00
or-else 3392bf5ed7 make sure unread is > 0 before playing a sound 2019-11-20 11:40:36 +03:00
or-else 99de97b2e1 pres.act is not set when the message is sent by the current user 2019-11-20 11:18:52 +03:00
or-else d30b616b26 bump version and rebuild 2019-11-18 09:25:30 +03:00
or-else aa7cb55c8e better presentation of unnamed contacts 2019-11-13 20:18:21 +03:00
or-else 9e756c6772 bump version and rebuild 2019-11-12 15:54:12 +03:00
or-else 8ee50adf4d fix base64 preprocessing 2019-11-12 15:53:52 +03:00
or-else 4c28753e6a show different message when search returned no contacts 2019-11-08 17:43:41 +03:00
or-else 1cdb8b724c rebuilt 2019-11-06 11:28:30 +03:00
or-else 6a2efac566 and nother place where title length is enforced 2019-11-06 11:27:49 +03:00
or-else 8c1b926008 enforce maximum title length for topics and users 2019-11-06 11:25:07 +03:00
or-else c539e5f70d another Russian translation fix 2019-11-06 11:24:45 +03:00
or-else 154ec5000e rebuilt 2019-11-06 11:03:52 +03:00
or-else 80c5103ce0 UI fix for excessively long topic name 2019-11-06 10:33:21 +03:00
or-else 15c0ff2e75 more idiomatic Russian translation 2019-11-06 10:33:02 +03:00
or-else a404734794 bump version and rebuild 2019-11-03 09:44:28 +03:00
or-else f962c8e8a9 updated dependencies 2019-11-02 09:58:34 +03:00
or-else bb20937bc9 replace unpkg with jsdeliver as unpkg is too unreliable https://github.com/mjackson/unpkg/issues/229 2019-11-01 09:41:06 +03:00
or-else 3026e21b46 updated Missing Functionality section 2019-11-01 09:39:38 +03:00
or-else 92b9faa063 explicitly ignore "upd" presence updates 2019-11-01 09:39:05 +03:00
or-else a1894871aa updated for the next tindroidX package 2019-11-01 09:37:56 +03:00
or-else bd03a7164e fix translation to most common spelling 2019-10-25 08:24:53 +03:00
or-else 9891620188 added "mobile-web-app-capable" meta header 2019-10-23 19:31:59 +03:00
or-else afa0726bdf bump version and rebuild 2019-10-21 09:12:59 +03:00
or-else 1a927e5420 rebuilt with fixed url 2019-10-16 13:05:58 +03:00
Gene 5f4db76cbf Merge pull request #41 from tinode/fix-privacy-link
Fix broken privacy policy link.
2019-10-16 13:04:15 +03:00
aforge 4092324fea Fix broken privacy policy link. 2019-10-15 23:34:56 -07:00
or-else efc7377942 rebuilt with tinode-js fix 2019-10-14 17:01:30 +03:00
or-else 94f88855de rebuilt with tinode-js 2019-10-12 09:40:04 +03:00
or-else 73afafb605 rebuilt because of change in tinode-js: handling of {pres what=term}, tinode/chat#278 2019-10-12 09:00:18 +03:00
or-else fe556a0707 mark tinode.dev.js and tinode.prod.js as binary 2019-10-10 11:59:20 +03:00
or-else 79d0c6dd14 attempted fix for tinode/chat#278 2019-10-10 11:57:23 +03:00
or-else 8e9349b58b split tinode-sdk into a separate file 2019-10-03 18:15:29 +03:00
or-else 22710adbe3 add spinner to login action, small build changes 2019-10-03 16:48:47 +03:00
or-else b8a1b39bba bump version and rebuild 2019-10-02 10:44:59 +03:00
or-else ef341cb94e destructive menu action wrapped in an alert 2019-09-27 16:55:19 +03:00
or-else cda001e5c6 fix: new group topic is immediately unsubscribed 2019-09-26 19:25:18 +03:00
or-else 254c4768eb badges were broken 2019-09-25 14:36:19 +03:00
or-else 8f0125587f add firefox scrollbar styling 2019-09-24 20:50:19 +03:00
or-else 1aaf6f8fab rebuilt after fixing big in deleting messages 2019-09-24 20:04:52 +03:00
or-else 1d56d55487 adding alert component 2019-09-24 19:40:36 +03:00
or-else 205ca95217 bug fix: archive view does not render 2019-09-24 19:26:59 +03:00
or-else f9a29ed3ef add alert for destructable actions 2019-09-24 17:12:38 +03:00
or-else f3420dc66f upgraded react-intl to 3.3 2019-09-23 15:28:22 +03:00
or-else 6d581c8e7c rename chinese translation from zh-CN to zh 2019-09-23 14:15:57 +03:00
or-else 10bb206d51 experimenting with code splitting 2019-09-22 21:32:32 +03:00
or-else a2747b6e33 clarify use of explicit UI human language 2019-09-22 21:32:06 +03:00
or-else b3b0ae59ab updated dependencies 2019-09-22 21:31:33 +03:00
or-else d99434000f clarify group member editing UI (disallow deletes for S only) 2019-09-22 21:30:50 +03:00
or-else 2a164ddf49 links for blocking/reporting users and topics 2019-09-22 21:19:04 +03:00
or-else 12a0660e5b shorthand replacements for React.Fragment and <span> 2019-09-22 21:18:31 +03:00
or-else 2502460143 translations added, minor visual tweaks, version bumped 2019-09-20 14:37:29 +03:00
or-else c31631b69f ToS added, small bug in CSS fixed 2019-09-19 12:57:42 +03:00
or-else 711cf7c690 fix: crash on leaving the topic 2019-09-04 20:12:15 +03:00
or-else 3669ed7645 bump version and rebuild 2019-09-03 14:55:07 +03:00
or-else 41ebf3c705 fix: #37; correct case for auto* in LoginView 2019-09-02 18:49:31 +03:00
or-else 6f359be64d make webpack path universal / vs \\ 2019-08-29 20:03:57 +03:00
or-else d472a321de no autocorrection for username 2019-08-26 12:26:10 +03:00
or-else eda9f0f7f9 fix: exception in send-message.jsx on Enter 2019-08-24 18:04:14 +03:00
or-else d9c729d9ab update dependencies and rebuild 2019-08-24 14:31:55 +03:00
or-else 78b0909f02 add open graph headers 2019-08-24 14:13:33 +03:00
or-else 61a9b6c56f minify logo.svg and bump version 2019-08-22 11:55:23 +03:00
or-else a6bc2a1f8f migrate the app from api.. to web.. 2019-08-22 10:46:14 +03:00
or-else 503c9128d4 fix: wrong bubble tip color in Safari 2019-08-21 17:12:47 +03:00
or-else 1b4bba051e fix blob: URLS, corractly calculate image preview viwport size 2019-08-17 20:07:15 +03:00
or-else c14fb857bc fix for possible xss exploits 2019-08-17 16:56:15 +03:00
or-else be528ff282 bum version and rebuild 2019-08-15 11:24:49 +03:00
or-else fb9461834a refactored getDerivedStateFromProps in in-place-edit 2019-08-15 10:49:24 +03:00
or-else 0217d84721 refactoring due to facebook/react/pull/15047 2019-08-15 10:30:29 +03:00
or-else f1744c3f69 getDerivedStateFromProps refactored in contacts-view and chips-input, #35 2019-08-14 20:24:07 +03:00
or-else a79373c8cd replace getDerivedStateFromProps with componentDidUpdate 2019-08-12 11:51:44 +03:00
or-else d2975f0c20 fix avatar not being updated while edited 2019-08-06 17:18:28 +03:00
or-else d782fb8eab bump version and rebuild 2019-08-02 09:06:46 +03:00
or-else bdf152e5d6 fix: unable to delete profile picture 2019-08-01 09:53:41 +03:00
or-else 40edad1c99 fixed: unable to remove topic avatar 2019-08-01 09:49:26 +03:00
or-else 7239aae8aa fix typo, bump versin and rebuild 2019-07-26 11:48:19 +03:00
or-else 81c789b713 push notification instructions updated 2019-07-20 12:12:26 +03:00
or-else 744da99f30 don't play new message sound for achived topics 2019-07-18 13:35:39 +03:00
or-else 112ce08fde updated dependencies and rebuilt 2019-07-17 13:51:59 +03:00
or-else 3fd01648a8 bump version and rebuild 2019-07-08 17:29:00 +03:00
or-else 619ec15920 update one translation 2019-06-26 16:08:54 -07:00
or-else a6a097b15f translations 2019-06-20 20:26:43 -07:00
or-else 450a2f6975 translation tweaks 2019-06-16 12:09:47 -07:00
or-else e006043290 rebuilt with possible fix for https://github.com/tinode/chat/issues/242 2019-06-15 09:46:39 -07:00
or-else 34db50280e rebuild 2019-06-13 09:41:31 -07:00
or-else 6268fb68f8 minor fixes 2019-06-12 18:27:24 -07:00
or-else 9a310a0438 handle credential confirmation from EditAccountView 2019-06-12 11:03:21 -07:00
or-else 821ab6a16f dependencies updated 2019-06-11 17:01:11 -07:00
or-else f9e78bf280 credential management 2019-06-09 19:57:41 -07:00
or-else 83488ca878 rebuild 2019-06-07 16:27:26 -07:00
or-else 53d748a7bd deleting contacts 2019-06-07 16:26:42 -07:00
or-else 6cb2b77f69 rebuild with updated sdk 2019-05-29 09:09:42 -07:00
or-else d133287e7e small translation tweaks, rebuilt 2019-05-29 08:57:42 -07:00
Gene 3ef14a6fe6 Merge pull request #32 from linsui/patch-1
Create zh-CN.json
2019-05-29 07:49:09 -07:00
linsui f84096488a Create zh-CN.json 2019-05-29 12:02:42 +00:00
or-else 884a822408 fix for broken UI when too many users are online in a topic 2019-05-21 10:29:10 +03:00
or-else fe000cec33 bump version and rebuild 2019-05-15 09:39:39 +03:00
or-else 3202dcb1b0 rebuid 2019-05-13 19:44:07 +03:00
or-else eb20ebff6e big fix: crash in updating tags due to missing arrayEqual 2019-05-13 19:43:26 +03:00
or-else a0e8aa5c54 add bold-italic font variant, rebuild 2019-05-11 19:23:31 +03:00
or-else 00e2642aa9 new section in readme 2019-04-23 20:09:27 +03:00
or-else 0396a617c8 bump version and rebuild 2019-04-14 18:10:31 +03:00
or-else 9a55e4b72e make letter tile background identical to ios & android 2019-04-13 10:27:01 +03:00
or-else 60a88bc3d5 bump version and rebuild 2019-04-09 16:11:17 +03:00
or-else e73c41f3c6 request messages when subscribing to a new topic 2019-04-08 16:11:43 +03:00
or-else 4b561de04c rebuild -rc2 2019-04-07 10:42:46 +03:00
or-else 35ec5b98cf better display of reconnect timer 2019-04-07 10:42:06 +03:00
or-else 4bd0052d9f add icon indicators for muted and blocked contacts 2019-04-07 09:28:54 +03:00
or-else 545b0c79cb bump version and rebuild 2019-03-30 11:44:52 +03:00
or-else c66bf89883 add missing import 2019-03-28 19:51:06 +03:00
or-else 60fa6d7806 add support for tags with special chars 2019-03-26 21:50:44 +03:00
or-else 194306ac4f add missing import 2019-03-25 18:04:23 +03:00
or-else f29305b0af bump version and rebuild 2019-03-21 09:58:29 +03:00
or-else 1ac74dbe36 update dependencies and rebuild 2019-03-14 15:43:51 +03:00
or-else c76953294c rebuild with the bug fix from tinode-js 2019-03-14 11:58:04 +03:00
or-else 1458184fc2 update versions and rebuild 2019-03-11 10:12:05 +03:00
or-else 14b63f4627 rebuild 2019-03-05 15:00:42 +03:00
or-else 9492455bc3 rebuild 2019-03-03 19:53:55 +03:00
or-else 32ad99128e more conststent check for peer restrictions 2019-03-03 19:37:16 +03:00
or-else 64f1fd4380 fix false "peer messaging disabled" 2019-03-03 15:00:56 +03:00
or-else 6386fc7a46 rebuild 2019-03-02 16:45:18 +03:00
or-else 29425b48fc add warning when peer's messaging is disabled 2019-03-02 15:52:58 +03:00
or-else a9ba11a0ff buf fix: missing fnd result after viewing account info 2019-03-01 13:26:10 +03:00
or-else d13b24263e prevent premature subscription 2019-02-28 15:28:54 +03:00
or-else 0f13f2edb9 rebuild 2019-02-28 13:57:29 +03:00
or-else a042db27e8 don't send {not} for own messages; explicit defacs in p2p {sub} 2019-02-28 13:56:35 +03:00
or-else ff619da2b7 force sane permissions for user-initiated subscriptions 2019-02-27 16:52:47 +03:00
or-else 93e6d388b6 fixes for inviting/blocking users 2019-02-27 15:22:35 +03:00
or-else 173d153b82 remove banned topic from view 2019-02-26 16:48:13 +03:00
or-else 0ddb3a4f75 rename readOnly -> !isReader etc 2019-02-26 16:25:30 +03:00
or-else 9193b9be19 revent premature subscription on reload 2019-02-24 19:45:02 +03:00
or-else 79970cf558 getting rid of UNSAFE_componentWillReceiveProps 2019-02-24 19:30:11 +03:00
or-else 5d17ef7039 add new chat acceptance handlers 2019-02-24 15:14:02 +03:00
or-else 7e0aa5b93b more actions without subscribing first 2019-02-24 11:08:52 +03:00
or-else fd32413ba1 more work on confirming topic subscriptions, update permissions without subscribing 2019-02-21 22:05:40 +03:00
or-else 22b277a4ad add invitation.jsx widget 2019-02-18 19:10:16 +03:00
or-else da65e5663f added actions for chat invitation. bubble shadows 2019-02-18 19:09:39 +03:00
or-else 76d5ee1ed8 bump version and rebuild 2019-02-15 13:08:01 +03:00
or-else e7d68b5052 a possible fix for FF not focusing on a tab after notification click 2019-02-14 18:16:17 +03:00
or-else 1a17b01f0c add skipWaiting 2019-02-13 15:28:26 +03:00
or-else d7fcd8c761 bump version and rebuild 2019-02-11 16:13:11 +03:00
or-else 5e0cf3a2d2 0.15.13-rc1 2019-02-11 16:12:06 +03:00
or-else 636e38a6d7 rebuild 2019-02-11 16:09:45 +03:00
or-else 17d7e94138 change const -> let 2019-02-09 17:43:57 +03:00
or-else 49af44d4ca upkg.com is supposedly fixed now, rolling back the workaround 2019-02-09 10:53:46 +03:00
or-else 588b157005 update react dependencies to 16.8.1 2019-02-09 10:53:10 +03:00
or-else b5f926ef96 update dependencies due to a security issue in lodash 2019-02-09 10:50:22 +03:00
or-else 2e110a7a78 make sure the push token is sent to the server on app reload 2019-02-08 12:43:44 +03:00
or-else f710e5c76c mark generated css as binary to exclude from diff 2019-02-07 10:58:18 +03:00
or-else bedcc9a923 rebuild and mark generated .js and .js.map files as binary 2019-02-07 10:55:32 +03:00
or-else a554c0a9e1 minor fixes in chip-input 2019-02-06 20:20:51 +03:00
or-else ca2b3624e5 rebuild 2019-02-06 20:09:28 +03:00
or-else 87947b3de6 archiving works, refactored UNSAFE_componentWillReceiveProps in chips-input 2019-02-06 20:08:50 +03:00
or-else 160e2574d4 correct accoount editing for incremental updates 2019-02-04 13:05:03 +03:00
or-else a534387593 do inscremental updates instead of full updates 2019-02-04 12:46:17 +03:00
or-else 004dec20d3 automatic un-archiving on new message 2019-02-03 19:11:35 +03:00
or-else 68c0faf527 propagate {info} message to "me" topic 2019-02-03 18:29:59 +03:00
or-else 6bd64b2863 cleal localStorage on logout 2019-02-03 17:18:44 +03:00
or-else fc96d200ed archiving kind of works but buggy 2019-02-03 12:48:47 +03:00
or-else 7e30c2acc6 temporary fox for broken unpkg unpkg/unpkg.com#176 2019-02-03 09:48:52 +03:00
or-else 541ead2136 implementing chat archiving (does not work yet) 2019-02-02 21:31:58 +03:00
or-else 6388fedd70 deep-copy update private and public fields 2019-02-01 18:37:07 +03:00
or-else f34c342db3 rebuild 2019-02-01 12:14:22 +03:00
or-else 1883d35779 Merge branch 'devel' of https://github.com/tinode/webapp into archiving 2019-02-01 12:13:38 +03:00
or-else 15793cad26 add "archive" menu" 2019-02-01 11:50:22 +03:00
Gene 5e3d5a4be2 Merge pull request #28 from maksfreetimer/patch-2
service-worker: error on "fetch" event
2019-02-01 10:43:17 +03:00
Max 5f1c1f9f2e service-worker: error on "fetch" event
Chrome 70.0.3538.110
Sometimes i get this error: `Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': 'only-if-cached' can be set only with 'same-origin' mode
2019-02-01 10:32:39 +03:00
or-else a4af2a53ac pubm versions and rebuild 2019-01-29 14:22:42 +03:00
or-else 9bbddc08e6 make broken image not clickable 2019-01-25 17:03:50 +03:00
or-else b0be1fd012 show sock broken_image when the image is invalid 2019-01-25 16:49:24 +03:00
or-else 21303346b1 0.15.12-rc3 2019-01-25 14:24:51 +03:00
or-else bd7531e5b7 fix for tinode/chat#212 2019-01-25 14:24:43 +03:00
or-else a0184ac002 add cosmetic space between words 2019-01-22 13:40:09 +03:00
or-else 4d438ccab4 use locale for dates too 2019-01-14 12:42:26 +03:00
or-else 48e6144568 bum version and rebuild 2019-01-13 14:47:42 +03:00
or-else e028503c9e add "broken_image.png" as fallback 2019-01-13 14:46:46 +03:00
or-else fcb3d78cc4 rebuild 2019-01-06 14:06:46 -08:00
or-else b976a3caa8 0.15.11 2019-01-06 14:02:45 -08:00
or-else cc6a4cd125 rebuild 2018-12-27 16:10:40 +03:00
or-else 6d7f98c55b 0.15.11-rc4 2018-12-27 16:08:31 +03:00
or-else d80ddcabea pass Enter event otherwise crash 2018-12-25 11:08:57 +03:00
or-else 4c44b61a9d bump version of a dependency 2018-12-23 14:49:26 +03:00
or-else 9eb7bebe80 bump version and rebuild 2018-12-17 12:08:27 +03:00
or-else 1f634f5877 remove unnecessary check 2018-12-17 12:06:33 +03:00
or-else f78d673e02 add "reconnect now" option 2018-12-17 12:02:47 +03:00
or-else a5d405b083 add handling og onAutoreconnectIteration 2018-12-17 10:05:37 +03:00
or-else 405ce756a2 allow avatar removal 2018-12-15 14:24:05 +03:00
or-else 31190ec826 rebuild 2018-12-15 12:59:51 +03:00
or-else 9b3c714390 js2016 changes 2018-12-15 12:11:22 +03:00
or-else 2e76b122a2 keep focus on message editing firld whenever possible 2018-12-15 12:05:03 +03:00
or-else 14d75223e2 remove debug logging 2018-12-13 22:22:32 +03:00
or-else bc3e82bcbf bug fixes: crash in editing group member permissions, fnd not working sometimes, var -> let, const 2018-12-13 15:42:54 +03:00
or-else 10d88da558 bump version and rebuild 2018-12-05 13:58:19 +03:00
or-else 4dde45b6be 0.15.10 2018-12-05 13:54:44 +03:00
or-else 00eeb2503a css fix 2018-12-01 15:51:21 +03:00
or-else 4f70de3aa9 make topic title required 2018-12-01 11:34:42 +03:00
or-else 486cf2e355 rebuild 2018-11-30 21:37:11 +03:00
or-else 21841c0022 add missing import 2018-11-30 21:36:02 +03:00
or-else d1f7f02fa0 correctlu display messages from missing users 2018-11-24 18:23:29 +03:00
or-else fbbab3e7d6 fixes in settings view and message deletion 2018-11-21 11:50:04 +03:00
or-else 7248b5c398 keeping scroll position on vertical resize works now 2018-11-19 16:05:49 +03:00
or-else 96de18ea7b 0.15.10-rc1 2018-11-17 21:34:20 +03:00
or-else 198bd9a64c keep message view in the same scroll position on vertical resize 2018-11-17 21:34:13 +03:00
or-else 9324faa32f rebuild 2018-11-17 12:39:44 +03:00
or-else 769c25170f bug fix, add maps to package 2018-11-13 12:32:46 +03:00
or-else 9e073356b2 remove unused code for form name 2018-11-11 21:25:00 +03:00
or-else 14ff124674 strip formatting from button text 2018-11-11 16:59:39 +03:00
or-else f959aa24cc new drafty formatting seems to work as expected 2018-11-09 14:59:48 +03:00
or-else 7d058df54d update screenshots 2018-11-04 19:03:18 +03:00
or-else c72819de38 forms seem to work now 2018-11-04 17:27:25 +03:00
or-else bad6232b9a more formatting of bot forms 2018-11-03 18:53:14 +03:00
or-else d0e4a52ba9 add source maps, forms rendered ok now 2018-11-03 16:29:57 +03:00
or-else 2b9883c254 remove unneeded param 2018-11-01 17:45:31 +03:00
or-else 00e48b7c93 add "update is available" check 2018-11-01 17:39:37 +03:00
or-else 20cf802d3d rebuild after merge 2018-11-01 08:48:59 +03:00
or-else b07a739619 merge 2018-11-01 08:46:41 +03:00
or-else 3a09df6c14 edited for clarity 2018-11-01 08:31:02 +03:00
or-else aa1b55c182 clarification 2018-10-31 22:09:52 +03:00
or-else e8dba6bd5e externalize dependencies 2018-10-31 22:00:18 +03:00
or-else c76a7dcb6b splitting out libraries 2018-10-31 19:44:07 +03:00
or-else e7f3b424e4 add <noscript> message 2018-10-31 12:12:10 +03:00
or-else 0198f446d4 service worker fetch event for Add To Home Screen 2018-10-30 22:31:10 +03:00
or-else c2f261b143 cosmetic fixes 2018-10-30 16:11:35 +03:00
or-else 8c4e0e9068 all components are translated now 2018-10-30 15:56:36 +03:00
or-else 954b0079a4 even more translations and fixes 2018-10-30 11:44:50 +03:00
or-else c8506eb143 more corrections and translations 2018-10-29 19:07:30 +03:00
or-else a48be912b4 bump version and rebuild 2018-10-29 13:37:35 +03:00
or-else 639755ea2b more components updated/translated 2018-10-28 14:27:59 +03:00
or-else dc838776ec more components updated/translated 2018-10-28 14:18:22 +03:00
or-else d7fb701983 more components are functional now 2018-10-27 20:01:20 +03:00
or-else 92d4ba8a86 EditAccountView translated 2018-10-27 12:42:32 +03:00
or-else 48d5732c16 more components updated and translated 2018-10-27 11:55:32 +03:00
or-else 71bbfb4fdd more fixes 2018-10-26 16:36:30 +03:00
or-else 782f2d1fea adding back missing and changed dependencies 2018-10-26 15:29:33 +03:00
or-else 25021a866d things seem to work 2018-10-26 14:45:50 +03:00
or-else 3e3dbd7fdb translation instructions 2018-10-26 08:55:37 +03:00
or-else 53d545e114 add links to translations 2018-10-25 18:58:46 +03:00
or-else 00a54ac0ee more fixes 2018-10-25 18:57:13 +03:00
or-else cbc89617dc change how version is extracted 2018-10-25 14:56:51 +03:00
or-else e139c1371e login page is displayed now 2018-10-25 14:02:53 +03:00
or-else 358f9eb970 webpack works now 2018-10-25 11:19:43 +03:00
or-else 765a6e5d84 add config file 2018-10-24 18:09:49 +03:00
or-else d4ee2fb7bc webapp.jsx is no longer needed 2018-10-24 18:09:05 +03:00
or-else 5958dec09b move config params into a separate file 2018-10-24 18:08:47 +03:00
or-else 99dfb0afeb sort components into widgets and views 2018-10-24 18:04:16 +03:00
or-else 5367c71784 add config files 2018-10-24 17:14:56 +03:00
or-else 7dead5b14d switching to webpack 2018-10-24 17:14:07 +03:00
or-else 2bb60ea7a2 add imports 2018-10-23 22:34:11 +03:00
or-else 4b7d76c150 split the code into components 2018-10-23 21:31:28 +03:00
or-else 8b02f39557 initial pass at internationalization 2018-10-23 16:39:01 +03:00
or-else a2f075f4fb first pass at localization 2018-10-23 14:09:39 +03:00
or-else 8a70f37446 more consistent use of single ' vs double '' quotes 2018-10-23 11:44:23 +03:00
or-else 47abebd1f2 bump version, update dependencies and rebuild 2018-10-22 09:30:56 +03:00
or-else 2e02f8a1c8 cosmetic changes, allow aditing of user"s default permissions 2018-10-21 16:36:01 +03:00
or-else 49c0fbfd78 fix brokn download link 2018-10-20 12:01:25 +03:00
or-else 37bad29350 upgrade, bump version, and rebuild 2018-10-16 10:43:37 +03:00
or-else 58c29a8248 add missing badge image 2018-10-12 10:27:19 +03:00
or-else cbc3875288 bump version and rebuild 2018-10-11 15:56:19 +03:00
or-else e43e3ffcbc ensure correct sorting even in presence of undefined values 2018-10-11 12:51:55 +03:00
or-else 57ca428a65 add bw badge for mobile pushes 2018-10-11 11:13:08 +03:00
or-else b2ffb626da start groupping notifications + notification click works now 2018-10-10 17:21:57 +03:00
or-else a1cb605f37 update versions and rebuild 2018-10-08 15:48:48 +03:00
or-else c7b2a30f24 crash on error 2018-10-06 13:50:01 +03:00
or-else da24e28a92 start handling MESSAGE_STATUS_FAILED 2018-10-06 12:44:24 +03:00
or-else c52b661c87 check if topic has unsubscribed while processing pending messages 2018-10-06 11:49:34 +03:00
or-else ca212fb038 try re-sending pending messages on reconnect 2018-10-06 11:47:41 +03:00
or-else 44039f9a82 add android asset links verification file 2018-10-04 17:23:16 +03:00
or-else bcee90ef06 correctly reset Tinode object on change in settings 2018-10-04 17:22:37 +03:00
or-else e712bff7b3 correctly re-initialize tinode after logout 2018-10-03 12:48:43 +03:00
or-else 57fa378f4d apply es6 -> es5 transform 2018-10-02 14:17:09 +03:00
or-else 6cbf536fa8 bump version and rebuild 2018-10-01 20:02:00 +03:00
or-else 13f492285e password reset works now 2018-10-01 19:31:50 +03:00
or-else 60968b84c6 cosmetic bugs 2018-09-30 21:43:10 +03:00
or-else 114bfc409f add password entry form 2018-09-30 21:28:28 +03:00
or-else 871a1e5b5b adding ability to reset the password 2018-09-30 14:22:02 +03:00
or-else 7a3212cf36 rebuild with the new firebase 2018-09-29 10:38:20 +03:00
or-else 82dda02b12 update firebase 2018-09-29 10:37:11 +03:00
or-else 76620cc9b5 final fixes for logout crash, bump version and rebuild 2018-09-29 10:10:54 +03:00
or-else dba9500825 reset title and favicon on logout 2018-09-27 20:26:14 +03:00
or-else 62cac29071 dont't crash on logout 2018-09-27 20:10:26 +03:00
or-else 07e095aa9a update dependencies and bump version 2018-09-24 17:15:48 +03:00
or-else c8a47efde2 move firebase credentials to a separate file 2018-09-24 12:37:35 +03:00
or-else 3ad8274ad8 fix for #19 2018-09-23 19:11:03 +03:00
or-else 64219b9343 0.15.6 release 2018-09-23 10:30:33 +03:00
or-else a6fe3e59d4 remove unnecessary css styling 2018-09-23 09:36:23 +03:00
or-else 0231de6086 bump version and regenerate files 2018-09-22 09:55:14 +03:00
or-else 22a2954eb8 0.15.6-rc5 2018-09-22 09:47:06 +03:00
or-else a884f6fea2 add missing comma 2018-09-21 16:49:54 +03:00
or-else 6ac0ad6fbf add missing service worker to package 2018-09-21 16:41:43 +03:00
or-else 15393198be ignore click for the current topic in found contacts 2018-09-21 13:09:59 +03:00
or-else c56c91afce simplify SearchContacts component 2018-09-21 09:56:44 +03:00
or-else 4a66c1ee73 regenerated files 2018-09-20 16:44:43 +03:00
or-else b9bdeeae19 finished up webpush, rearranged settings 2018-09-20 16:42:46 +03:00
or-else c14a2044cd push notifications work now 2018-09-19 17:52:29 +03:00
or-else 72aef53c7e use explicit fb version as in service worker 2018-09-19 17:42:50 +03:00
or-else c567a2678a push notifications seem to work in FF and Chrome 2018-09-19 17:38:30 +03:00
or-else 242b30cee0 adding firebase messaging 2018-09-19 11:57:57 +03:00
or-else 6c9baf3cba merged from devel 2018-09-18 19:37:36 +03:00
or-else 534a2fded7 simplified version reporting, bump version 2018-09-18 19:35:41 +03:00
or-else 2fc6bebacc incorrect type of "private" in new topics 2018-09-18 16:47:46 +03:00
or-else e761c5e0f6 simplify version reporting 2018-09-18 09:57:10 +03:00
or-else 1a00f4b844 bump version 2018-09-18 09:29:40 +03:00
or-else ab7a049512 rebuild generated files 2018-09-17 19:12:12 +03:00
or-else bffa6eb6ce add missing react-dom requirement 2018-09-17 19:11:08 +03:00
or-else cefde649ba add missing react-dom requirement 2018-09-17 19:10:51 +03:00
or-else 89c41818f0 Merge branch 'devel' into push 2018-09-17 16:29:21 +03:00
or-else f53cef6262 rename the app 2018-09-17 10:03:46 +03:00
or-else 3064c0ec2a start adding push notifications 2018-09-16 20:20:22 +03:00
or-else 1e268b3eae replace buggy tinify with uglify-es 2018-09-16 15:04:20 +03:00
or-else 7228053261 remove demo login and password hints from the login screen 2018-09-16 10:15:16 +03:00
or-else d6d9340304 rebuilding again without the prepublish 2018-09-14 16:21:17 +03:00
or-else 2bf4a0fe1f mention the app size 2018-09-14 16:18:42 +03:00
or-else f9725cb06a package TinoeWeb as npm module 2018-09-14 16:13:41 +03:00
or-else 3c20bcf6f2 fixes for various UI bugs 2018-09-13 17:10:11 +03:00
or-else 03db847d48 last fix for the attachments, bump version 2018-09-12 17:19:11 +03:00
or-else 8b71444341 fix class name 2018-09-11 20:47:05 +03:00
or-else ce20a86072 fix react-js warnings 2018-09-11 20:44:21 +03:00
or-else c768755ae4 fix for issues/17 2018-09-11 16:55:30 +03:00
or-else ef3d799411 merge chat list and found contacts 2018-09-11 15:42:05 +03:00
or-else 0ba4b85a91 Merge branch 'master' of https://github.com/tinode/webapp into repackage 2018-09-11 15:39:57 +03:00
Gene ad016b2a8e Merge pull request #18 from maksfreetimer/patch-1
GroupManager methods fix
2018-09-11 15:39:30 +03:00
Max 18c7a82ce6 GroupManager methods fix
"selectedContacts" method seems to be called statically, otherwise "handleContactSelected" & "handleMemberRemoved" methods throw an exception
2018-09-11 15:04:41 +03:00
or-else dd9fd8e345 bug due to a typo 2018-09-09 11:46:57 +03:00
or-else 1318f399e6 typo in comment 2018-09-09 10:40:05 +03:00
or-else 6ed768cacc refactoring completed 2018-09-08 21:17:18 +03:00
or-else 7d01356be7 moving contact management to <TinodeWeb /> 2018-09-08 19:51:50 +03:00
or-else 72e51b65a2 remove debug logging 2018-09-07 09:54:10 +03:00
or-else b282f789fb replacing global Tinode object with TindeWeb's state object 2018-09-06 20:55:39 +03:00
or-else a578e27c82 refactor context menu 2018-09-06 12:00:01 +03:00
or-else e23b3172c9 start tracking generated files 2018-09-05 18:58:12 +03:00
or-else 5e0713fb3e partially updated Tinode from a global object to a state one 2018-09-05 18:56:01 +03:00
or-else aafa0f74d5 trying to package the webapp 2018-09-04 19:28:46 +03:00
or-else 40000cfa44 try to make this work for both devel and prod 2018-09-04 16:37:55 +03:00
or-else 3865eb8928 Merge branch 'devel' into repackage 2018-09-04 16:27:17 +03:00
or-else 7bf26a55be desktop -> notification alearts 2018-09-04 16:26:53 +03:00
or-else 15e5dacc51 adding desktop alerts, React and js updates 2018-09-04 16:20:34 +03:00
or-else 2de87accf5 start using onAllMessagesReceived 2018-09-04 12:44:52 +03:00
or-else 78764319c0 0.15.5 2018-09-03 12:20:28 +03:00
or-else cf86af1284 change prompt wording on validation code 2018-09-02 14:32:28 +03:00
or-else 22c86747a1 update version 2018-09-01 14:25:36 +03:00
or-else 39b18ff6dd generated files updated 2018-08-28 09:35:41 +03:00
or-else f833ec6960 correct error reporting on credential validation 2018-08-28 09:32:14 +03:00
or-else cb4a0655a3 bump version 2018-08-27 14:33:45 +03:00
or-else 8354d921f3 bump version 2018-08-25 16:49:35 +03:00
or-else d178803d86 rebuilt derived files 2018-08-25 16:48:40 +03:00
or-else 4aadff8ab3 message sounds, fix in searching for contacts 2018-08-25 16:43:27 +03:00
or-else cf849d6412 rearranging files 2018-08-24 16:00:07 +03:00
or-else 5c4429858b adding explicit dependencies on tinode-sdk 2018-08-24 14:25:55 +03:00
or-else 996dad96c1 minified 2018-08-21 14:58:40 +03:00
or-else 55c08373d0 making credential verification work better 2018-08-21 14:58:09 +03:00
or-else aca9286d63 if a png image is too big even after scaling, convert it to jpeg 2018-08-21 11:53:03 +03:00
or-else c7325e3c25 change var to const where appropriate 2018-08-21 10:08:25 +03:00
or-else 61b3bcbb4f corrected start_url 2018-08-21 09:33:22 +03:00
or-else 758b5b54cf changes to message deletion and inband image size 2018-08-20 18:51:37 +03:00
or-else 94c8bab4ec getting rid of componentWillMount per React 16 specs 2018-08-19 19:44:14 +03:00
or-else 7ac3041493 bump version and rebuild 2018-08-19 19:30:28 +03:00
or-else 4be41e47f4 starting to get rid of componentWillMount per React spec 2018-08-19 11:22:43 +03:00
or-else f782bc11cc simplifies InfoView parameters 2018-08-19 10:58:22 +03:00
or-else be8d64e29e generated files updated 2018-08-18 09:16:39 +03:00
or-else e22a4de007 remove unnecessary state update 2018-08-18 09:03:42 +03:00
or-else fde40f4f51 build and bump version 2018-08-17 16:07:03 +03:00
or-else e2b19d72bc add css minification 2018-08-13 11:36:50 +03:00
or-else a0eac497c6 minor npm packege changes 2018-08-12 16:13:52 +03:00
or-else f2ef14c239 setting up offline babel transpiling and nmp-ifying 2018-08-12 11:03:26 +03:00
or-else 2b8514b495 fix for visual breakage (https://github.com/minimaxir/big-list-of-naughty-strings) 2018-08-10 16:42:41 +03:00
or-else e9e8d9881f make the webapp work with browserify-processed tinode.js (symlinked tinode.dev.js) 2018-08-10 15:16:40 +03:00
or-else 3975401205 fix canonical url and application-url 2018-08-09 16:56:53 +03:00
or-else 6da0952e5d adding package.json 2018-08-09 10:48:51 +03:00
or-else 45acee4617 fix for a broken "send" button 2018-08-08 21:07:18 +03:00
or-else ff5096cd47 multiple simultaneous uploads and downloads work now 2018-08-08 15:59:26 +03:00
or-else 4cd97e7ff8 refactoring upload progress 2018-08-07 20:55:55 +03:00
or-else df86411cb7 rearranging components, splitting js/babel from html 2018-08-07 12:15:10 +03:00
or-else 905501f22f use delayed message sending 2018-08-06 08:43:30 +03:00
or-else c48e5f1dbf don't print entire message 2018-07-30 21:23:43 +03:00
or-else 0a956f0234 don't crash on malformed attachments 2018-07-30 20:50:48 +03:00
or-else ae73764173 fix: exception while logging out 2018-07-29 19:14:41 +03:00
or-else 2f8e5c4b33 conversion to react 16 seems to be successful 2018-07-27 19:27:26 +03:00
or-else a542afd194 updating reactjs to version 16 2018-07-27 18:13:05 +03:00
or-else a8c9347591 remove debug output 2018-07-14 14:15:47 +03:00
or-else dc46f8f166 bug fix: incorrect handling of avatar pic in InfoView 2018-06-20 12:19:44 +03:00
or-else 6eb3551665 add attachment's URLs to message header 2018-06-15 14:34:06 +03:00
or-else 3829b26cc8 fix an issue with incorrect scaling of png images 2018-06-11 11:31:18 +03:00
or-else 973b71eb80 uploading and downloading works ok now 2018-06-10 18:55:18 +03:00
or-else 1b7fd6a00b file uploads mostly work now 2018-06-09 21:14:12 +03:00
or-else daa1ee3e0c send credentials only if download url is relative 2018-06-09 19:05:26 +03:00
or-else 35eb23e3af uploading works with some minor UI glitches 2018-06-09 11:13:04 +03:00
or-else 0c16a5c991 uploading works, downloading still does not, UI broken 2018-06-08 22:15:31 +03:00
or-else 926dd43ce6 fixed image preview, split max inband size from total max attachment size 2018-06-06 17:47:48 +03:00
or-else 499bf1ee14 separate download url from preview url 2018-06-06 16:41:16 +03:00
or-else 9c507ba631 update Drafty for external attachmanes, update UI 2018-06-06 16:11:50 +03:00
or-else 2f307e2803 making file upload widget more consistent visually 2018-06-06 10:15:39 +03:00
or-else 2c21a34d07 adding large file uploads 2018-05-31 21:41:06 -07:00
or-else 2e3ecb1c4e support copy-pasting images and file attachments 2018-05-23 13:29:31 -07:00
or-else e21bf82de1 inline documentation cleanup 2018-05-22 18:54:04 -07:00
or-else d837d8950c start treating .Private as a dictionary instead of string 2018-05-19 16:51:16 -07:00
or-else b17e0d45ae updated logos (completely transparent background 2018-05-18 15:40:15 -07:00
or-else f5af1ff193 updated search, better typing indicator 2018-05-18 14:12:50 -07:00
or-else e77513d44b more meta tags and "keep me loggen in" on account creation 2018-05-17 09:07:26 -07:00
or-else 556e249b78 bug fix: incorrect handling of empty hashpath 2018-05-16 19:58:39 -07:00
or-else ce0e83070a don't send read notifications if tab or windows is invisible 2018-05-16 17:26:04 -07:00
or-else d0747b9f7b bug: topic may not be ready yet 2018-05-16 11:55:48 -07:00
or-else eb3ccf7b1e bug fix: crashes when switching topics with open <InfoView> 2018-05-16 11:36:54 -07:00
or-else 2e619d3fb8 show unread threads in title, bug fix 2018-05-16 10:17:00 -07:00
or-else 9d128e505f switch account tags from "me" to "fnd" 2018-05-15 10:32:32 -07:00
or-else 77479a3e2d graphic assets updated 2018-05-14 20:27:13 -07:00
or-else 353bd1d5fe bumped version to 0.15 due to incompatible changes in in FND 2018-05-14 13:05:37 -07:00
or-else ed11dd0d22 catch and report the error instead of throwing an exception 2018-05-10 17:08:44 -07:00
or-else 189fc0e3f2 new search works now 2018-05-09 19:00:15 -07:00
or-else d5c7464570 css tweaks 2018-05-05 10:22:58 -07:00
or-else ce4c803b97 handling of very long strings 2018-05-04 13:22:20 -07:00
or-else c6dbe3830e bug fix: callback to a stale ContactsView 2018-05-03 13:35:33 -07:00
or-else 09a1b85e86 bug fixes in creating topic with tags 2018-05-03 10:52:54 -07:00
or-else 82cc9ebb09 better handling of unchanged tags 2018-05-02 11:22:07 -07:00
or-else 820a4081a1 fix message scrolling and no-read-access display 2018-04-27 20:14:44 -07:00
or-else 09eb4bdde8 correct redirect after failed login 2018-04-27 10:52:43 -07:00
or-else c9c6bf1cef correctly display an invalid topic 2018-04-26 21:45:29 -07:00
or-else ff67a6d3d2 yet even more bug fixes in navigation 2018-04-26 14:13:18 -07:00
or-else c110c4bb5e active topic is not longer disabled on startup 2018-04-26 11:23:35 -07:00
or-else e2be203088 still debugging hash navigation 2018-04-25 21:22:44 -07:00
or-else 316de3e875 start actually using "keep-logged-in" 2018-04-25 10:57:07 -07:00
or-else 22c92043a1 another small fix for navigation 2018-04-25 09:39:19 -07:00
or-else cd4388d574 more debugging of hash navigation 2018-04-24 20:32:43 -07:00
or-else 9449606adc navigation mostly works but still buggy 2018-04-24 14:29:15 -07:00
or-else 4101ae21d1 Merge branch 'master' of https://github.com/tinode/example-react-js into devel 2018-04-23 21:03:20 -07:00
or-else 15ffade573 a few more navigation fixes 2018-04-23 20:44:24 -07:00
or-else 8148d42ba7 navigation by hash works partially 2018-04-23 19:48:57 -07:00
or-else 0f08e63b6e adding routing to hashchange handler 2018-04-23 12:41:02 -07:00
or-else 39b5ce8210 bug fix: remve &tab= on cancel 2018-04-22 16:26:26 -07:00
or-else a364aa7dea hash reflects the app state now 2018-04-22 16:21:17 -07:00
or-else e5fc770369 more work on #hash navigation 2018-04-22 14:24:26 -07:00
Gene da05994d0f Adding Apache 2.0 license 2018-04-21 21:46:21 -07:00
or-else 18ef1e88fd making the app navigatable by #hash 2018-04-21 18:23:56 -07:00
or-else 7e5f7dd30b adding local storage for auth token 2018-04-21 13:29:49 -07:00
or-else decad8f65c sort conversation by the last message timestamp 2018-04-19 19:16:59 -07:00
or-else 6edaf16db8 start reporting an error on invalid topic id 2018-04-15 14:06:03 -07:00
or-else cd8f5a1b94 remove badges from contact list 2018-04-15 12:55:10 -07:00
or-else 586b0f152b bug fixes in InfoView 2018-04-13 21:20:30 -07:00
or-else d06ca5cdbd bug fixes around acs handling 2018-04-11 17:42:26 -07:00
or-else ae1f08e506 don't send permissions update if permissions have not changed 2018-03-30 20:28:34 -07:00
or-else 53ef0f83d6 bug fix: wrong online status of a new topic 2018-03-30 10:55:37 -07:00
or-else efcf2680ec check for changes, don't send if unchanged 2018-03-28 19:19:18 -07:00
or-else ba4f976e3b cancel tag editor on escape key 2018-03-28 16:39:22 -07:00
or-else 757b23356c topic tags work as expected now 2018-03-28 16:29:45 -07:00
or-else fa90ff5bef make extra parameters collapsible to make more space for tags 2018-03-28 12:33:31 -07:00
or-else 60262291ce tags can now be added to group topics 2018-03-28 11:31:51 -07:00
or-else 515e27b489 refactoting tag manager 2018-03-28 11:04:45 -07:00
or-else ed268053f3 bug fix: in some cases messges never marked as read 2018-03-26 19:46:13 -07:00
or-else f68d4d3e24 bug fix in showing online group subscribers 2018-03-26 11:03:34 -07:00
or-else 00dd18302f bug fix with unread count, css & html fixes 2018-03-25 12:45:01 -07:00
or-else 3cba22ae22 disable notification sound for now 2018-03-24 18:45:56 -07:00
or-else ac89af9850 play sound on new message 2018-03-24 11:21:44 -07:00
or-else 83e17861bd allow the same file to be attached, bug in login 2018-03-23 19:36:59 -07:00
or-else 67c5cbf77f clear error on panel switch 2018-03-23 16:50:04 -07:00
or-else 1e69fabe2c better validation of input 2018-03-20 17:38:48 -07:00
or-else 9283c4ede2 minor corrections 2018-03-17 18:57:10 -07:00
or-else 95b9f1a1fd renaming type -> meth 2018-03-16 18:49:01 -07:00
or-else 99bab6cae9 added validation screen 2018-03-15 18:52:31 -07:00
or-else 57490cbe92 adding verification view 2018-03-14 20:22:04 -07:00
or-else 6d8b49c36c added autocomplete= to login, tag length enforcement 2018-03-09 11:49:19 -08:00
or-else b7a4efa6b6 tag management works now 2018-03-04 18:11:56 -08:00
or-else dac21cf807 fix favicon display 2018-03-03 18:59:28 -08:00
or-else bacaadfd86 added UI for tag handling 2018-03-03 18:24:13 -08:00
or-else ea55bde395 adding tag processing 2018-03-03 16:27:10 -08:00
or-else b277b020f7 more debug output 2018-02-28 14:24:09 -08:00
or-else 1415f21c51 bug fix in display of online topic subscribers 2018-02-12 15:04:33 -08:00
or-else 576d81a7ec adding back accidentally deleted link to manifest.json 2018-02-05 10:41:27 -08:00
or-else 0e5fb5b468 more icons 2018-02-05 10:38:09 -08:00
or-else 8daf217795 adding icons and actual manifest.json 2018-02-05 10:19:57 -08:00
or-else 29a55c1e37 adding manifest.json and related 2018-02-05 10:18:53 -08:00
or-else 964a423879 removed logging and renamed a few methods for clarity 2018-02-04 18:51:38 -08:00
or-else addd27108f making topic sach work 2018-02-04 18:14:29 -08:00
or-else dc91e9d7b7 report an error on failed invite 2018-01-22 17:21:51 -08:00
or-else c80e09d956 remove debug console logging 2018-01-20 17:33:58 -08:00
or-else 6a2681ab56 show users currently online in a topic 2018-01-20 17:24:34 -08:00
or-else e5c98ba2d1 bug fix for an "read" status not being sent in case of deleted messages 2018-01-09 15:13:57 -08:00
or-else 1b9b7a2671 bump app version 2017-12-14 11:18:41 -08:00
or-else 1a3362f1cd fix online status 2017-12-05 22:23:18 -08:00
or-else 9009667c5a bug fix: correct handling of hard-delete 2017-12-03 14:43:54 -08:00
or-else a59023575b bug fix in topic member menu 2017-11-25 19:02:36 -08:00
or-else b2b1ca2934 kind of works but buggy 2017-11-25 14:24:18 -08:00
or-else afd1bbb58a making new delete scheme work 2017-11-24 20:23:49 -08:00
or-else de655a105a bug fix 2017-10-25 14:00:21 -07:00
or-else 2c1c0b2544 fix for very long file names 2017-10-22 17:37:43 -07:00
or-else ccf5318e4c mention than deleting messages works now 2017-10-13 16:42:46 -07:00
or-else 7322abfd55 messages can be deleted one by one now 2017-10-13 13:04:54 -07:00
or-else 09518bbac2 adding message deletion - incomplete yet 2017-10-13 10:23:19 -07:00
or-else b6a831ec91 big fix in error handling 2017-10-01 16:08:02 -07:00
or-else 378cd622c4 dropping support for .ico because they are not well supported on android 2017-09-30 22:03:05 -07:00
or-else 939e3e59cf attach some images without converting them first 2017-09-30 16:47:02 -07:00
or-else 22e82b1c7f corrected inline image width on mobile 2017-09-26 14:54:00 -07:00
or-else e495917356 simplified handling of inline images 2017-09-26 14:43:46 -07:00
or-else cf4b58cab2 added missing screenshot 2017-09-25 21:01:36 -07:00
or-else 0a96014484 inline images work now 2017-09-25 21:00:35 -07:00
or-else 3e8fd9d6e6 file attachments work now 2017-09-24 18:21:14 -07:00
or-else a06d29863d add UI elements for attaching images and other docs 2017-09-23 13:26:58 -07:00
or-else de24e23127 bug fix: exception in changing permissions 2017-09-21 19:49:21 -07:00
or-else 9417e1aa49 bug fix: correct display of online status in MessagesView 2017-09-21 19:38:00 -07:00
or-else 1f8d26a215 bug fix: send private value in topic creation 2017-09-21 18:23:26 -07:00
or-else e4a0b60f1e better indication of write-only topics 2017-09-21 09:41:24 -07:00
or-else 19612e4236 add blob generation from base64 to be able to deal with large images 2017-09-20 17:13:13 -07:00
or-else af1e9c609b forgot to add the new icon 2017-09-20 15:27:37 -07:00
or-else 5d18c71475 add green dot to shortcut icon to indicate unread messages 2017-09-20 15:01:19 -07:00
or-else 20fc9ae343 scroll only on material changes 2017-09-20 12:00:54 -07:00
or-else 2e8b9145b7 better handling of lost/restored connection 2017-09-19 20:15:40 -07:00
or-else 859dec8410 removed dead code 2017-09-17 20:57:19 -07:00
or-else 8f13d2c856 better error handling and reporting 2017-09-17 18:36:01 -07:00
or-else d2a226852b fixed drafty's handling of undefined values 2017-09-15 18:04:02 -07:00
or-else 9c7a05f616 bug fix: lost topic parameters after reconnect 2017-09-14 19:26:45 -07:00
or-else a6e59936e2 typing indicators work now 2017-09-14 15:36:34 -07:00
or-else e3d95eafe1 bug fixes 2017-09-14 11:43:27 -07:00
or-else e30568ecd5 correctly propagate update to all components 2017-09-14 11:09:25 -07:00
or-else 9ac8ea8f93 adding borders to screenshots 2017-09-13 14:06:31 -07:00
or-else 1bac018961 adding borders to screenshots 2017-09-13 14:05:54 -07:00
or-else 1aba91d6f3 adding borders to screenshots 2017-09-13 14:03:20 -07:00
or-else fd750558ac adding borders to screenshots 2017-09-13 14:00:19 -07:00
or-else f2cb7d1e8b updated screenshots 2017-09-13 13:36:07 -07:00
or-else 7870fb06a2 better use of local cache 2017-09-13 12:17:24 -07:00
or-else 9adff7572e message pagination 2017-09-12 18:21:33 -07:00
or-else d50da89403 updated screenshot 2017-09-07 19:21:08 -07:00
or-else 8ddfb93f7a topic invites work correctly now, UI fixes 2017-09-07 19:10:50 -07:00
or-else b718aa9513 more context menu fixes and features 2017-09-05 22:04:14 -07:00
or-else 7782021c12 refactoring context menus 2017-09-03 21:52:59 -07:00
or-else 73b2a85eea cleaning up drafty and UI fixes in permissions editing 2017-09-02 15:57:03 -07:00
or-else d34f51a9d0 better formatted example 2017-08-24 20:16:53 -07:00
or-else 0a2abc080f a lot of refactoring of drafty to male it usable as a universal media container 2017-08-24 12:35:14 -07:00
or-else 9d1786ab67 better error handling 2017-08-21 19:53:41 -07:00
or-else fb6b4899cd rich formatting works 2017-08-21 19:45:48 -07:00
or-else 0c7549819a adding rich formatting to messags 2017-08-19 20:40:25 -07:00
or-else fd8c58e837 bug fix 2017-08-18 11:20:20 -07:00
or-else 5c74be339a added a couple of supporting methods and made representation a bit more compact 2017-08-18 11:01:51 -07:00
or-else c88c821002 added samples 2017-08-17 13:20:20 -07:00
or-else f20f83b936 urls, mentions, and hashtags are extracted and formatted correctly 2017-08-17 13:07:32 -07:00
or-else 88cc3145c4 parser and formatter work as expected 2017-08-16 17:53:39 -07:00
or-else 5d1bf5f624 first pass at defining rich text formatting 2017-08-15 20:54:59 -07:00
or-else 9fa998709d small tweaks to the logo 2017-08-10 22:35:20 +03:00
or-else 0f90626a49 make permissions editor UI clearer 2017-08-09 10:30:59 +03:00
or-else 40108d5201 incorrect value used for user2 2017-08-08 22:13:29 +03:00
or-else 0dde03cf6f more refactoring of permissions editor. still does not work 2017-08-08 18:33:30 +03:00
or-else 83fe8ea02b refactoring permissions editor 2017-08-08 11:16:50 +03:00
or-else 17876fc4b0 bug fix: an extra copy of a p2p topic was sometimes created 2017-08-07 21:14:42 +03:00
or-else eca16c8474 making default permissions work as expected 2017-08-07 17:12:43 +03:00
or-else ff0dfed2e8 change auto-login order: try token first 2017-08-07 10:06:03 +03:00
Gene db8e718002 Merge pull request #6 from riandyrn/attempt-generalized-number-of-tags-sent-to-server
[Attempt to Generalize Number of Tags Sent to Server]
2017-08-05 13:05:14 +03:00
Riandy Rahman Nugraha f49f0424d5 generalized to_tag() into to_tags() 2017-08-05 16:59:56 +07:00
or-else 998ddfd54b changed new code for style and made it usable for other non-email tags 2017-08-04 14:47:18 +03:00
Gene 53e67de669 Merge pull request #5 from riandyrn/riandy-branch
[Attempt to Make New User Discoverable]
2017-08-04 14:37:36 +03:00
Riandy Rahman Nugraha 897bb0719a remove tinode.js from example app —> tinode.js already has its own repo 2017-08-04 09:11:29 +07:00
Riandy Rahman Nugraha d051c84737 explicitly set defacs for newly created account to “JRWPS”; add email as tags in newly created account, so it could be discovered by others; 2017-08-03 09:15:31 +07:00
or-else 578df160d1 cosmetic change 2017-07-23 18:23:35 +03:00
or-else ac335a7a5c newer screenshot 2017-07-15 18:22:53 +03:00
or-else 5868915f2c permissions editing works now 2017-07-15 12:35:53 +03:00
or-else 1054c81de3 Merge branch 'devel' 2017-07-10 20:34:40 +03:00
or-else 79756602a7 topic member management complete 2017-07-10 19:29:35 +03:00
or-else 8d5f446ccd member management is complete now 2017-07-09 11:22:27 +03:00
or-else b689d08e71 subscriber management is somewhat functional 2017-07-06 16:15:56 +03:00
or-else 58ae90b574 added mateerial chips widget 2017-07-04 19:23:07 +03:00
or-else e79444e5f7 lets of bug fixes 2017-07-02 16:16:45 +03:00
or-else 45eb91765b more refactoring of topic and account editing 2017-06-29 14:02:16 +03:00
or-else 933ede1220 separated contact list into a component 2017-06-27 20:58:27 +03:00
or-else 506bd42201 documentation update 2017-06-25 20:40:17 +03:00
or-else b3ef0a856b name and version 2017-06-25 15:33:20 +03:00
or-else 732c17ad76 make mobile screenshots bigger 2017-06-25 15:32:15 +03:00
or-else 8ee22694f1 fit three images in a row 2017-06-25 15:29:25 +03:00
or-else e3909c96bd bug fixes and visual clean up 2017-06-25 15:26:19 +03:00
or-else ac97f1d61d added topic and account editing 2017-06-24 21:03:37 +03:00
or-else 61fbc69356 refactored contect menu and updated readme 2017-06-22 21:04:12 +03:00
or-else 67cd659712 better handling of avatars 2017-06-16 17:44:17 -07:00
or-else 895a81799f clearing messages works now 2017-06-15 21:29:07 -07:00
or-else 097bf7d193 make avatar circular rather than oval 2017-06-13 18:45:11 -07:00
or-else 40cccfdfa9 added error handling for hello() 2017-05-24 18:23:26 -07:00
Gene S 71fc855940 better handling of runtime server address changes 2017-04-25 11:49:12 -07:00
Gene S 9fc05d97d9 making host selectable from the login screen 2017-04-20 17:50:23 -07:00
Gene S 0a3dfa8d9a change server address to public 2017-04-19 17:05:12 -07:00
Gene S 43016e389a refactoring access mode to support server changes 2017-04-18 17:41:39 -07:00
Gene S 8c4284a435 Merge branch 'master' of https://github.com/tinode/example-react-js 2016-09-09 18:14:55 -07:00
Gene S 3f13c56fb9 merging changes 2016-09-09 18:14:52 -07:00
Or 73e2be8ac8 Merge pull request #1 from npmcdn-to-unpkg-bot/npmcdn-to-unpkg
Replace npmcdn.com with unpkg.com
2016-09-08 12:27:02 -07:00
npmcdn-to-unpkg-bot 86f706dee7 Replace npmcdn.com with unpkg.com 2016-09-08 07:29:29 +01:00
Gene S 1372a68ecf a few fixes around contacts discovery 2016-09-06 19:24:58 -07:00
Gene S dc37ab3d7a updated screenshot 2016-09-02 10:52:49 -07:00
Gene S e9de7d89fd contact discovery works now 2016-08-29 18:04:52 -07:00
Gene S 2e4b165b42 updated reactjs to the latest version 2016-08-29 14:54:50 -07:00
Gene S d03b340985 updated reactjs to the latest version 2016-08-29 14:54:28 -07:00
or-else 90d579bbcb incompatible protocol changes in authentication, version bumped to 0.8 2016-08-28 17:12:16 -07:00
Gene S 8067985edd bugs: AccessMode was broken, muting topics is now possible, but un-muting is still broken 2016-08-18 09:58:23 -07:00
Gene S 2d5ed1bb83 bug: removing deleted topic from view 2016-08-17 11:06:20 -07:00
Gene S 85bcc107d4 updated reactjs to the latest version 2016-06-15 15:40:58 -07:00
Gene S 3d4285d2dc introduced a handshake packet and bumped protocol version to 0.7, a few bugs fixed. long polling is broken 2016-06-15 15:28:22 -07:00
Gene S c7282f8c04 small bug fix in logging and clearer isplay of versions 2016-06-07 19:00:51 -07:00
Gene S e06612f5b3 autoreconnect with backoff and bug fixes 2016-03-16 19:16:31 -07:00
Gene S e28655a64c finished topic creation/deletion 2016-03-10 18:54:26 -08:00
Gene S 1db21526e1 fixes around deleteing/unsubscricing p2p topics 2016-03-09 17:26:55 -08:00
Gene S d709739dc0 menu positioning fixes 2016-03-04 18:31:15 -08:00
Gene S d9a1e2fd0c bug fixes 2016-03-04 18:12:06 -08:00
Gene S ac86e415e9 topic creation and context menu fixes 2016-03-03 18:03:11 -08:00
Gene S 5b89abb646 user search (incomplete) 2016-02-29 18:49:39 -08:00
Gene S ed49b8b10f user discovery 2016-02-29 09:32:38 -08:00
Gene S 58d02cf4bc added browser styling 2016-02-23 11:11:51 -08:00
Gene S 0ed884f5ad reduced delay fore sending read confirmation 2016-02-19 18:49:13 -08:00
Gene S e278ebda5d bug fixes 2016-02-18 19:30:21 -08:00
Gene S 103184c8da Merge branch 'master' of https://github.com/tinode/example-react-js 2016-02-17 19:06:45 -08:00
Gene S 3159ddb0a6 refactored message background colos 2016-02-17 19:06:42 -08:00
Gene S fc7369f586 refactored message background colos 2016-02-17 19:05:43 -08:00
or-else 11c78eb85e merge conflict + update react version to 14.7 2016-02-14 12:10:29 -08:00
or-else 5449728cc6 Changed public: vcard: <data> to public: <data> 2016-02-14 12:03:25 -08:00
Gene S 7d8e58fb4d changed host name 2016-02-12 18:22:29 -08:00
Gene S 1d46fd0279 refactoring of {sub} {get} and {set} for clarity 2016-02-12 15:52:05 -08:00
Gene S cc99b267bb swapping host name 2016-02-01 09:52:22 -08:00
Gene S cf38f55291 swapping host name 2016-02-01 09:50:18 -08:00
or-else 1483740189 making the app adaptive - now works on mobile 2016-01-31 20:04:37 -08:00
or-else f5e91a6366 update readme 2016-01-29 20:44:42 -08:00
or-else 0e539c0a98 change host from local to remote 2016-01-29 20:16:17 -08:00
Gene S d1265c21ee adding avatar support 2016-01-29 19:23:02 -08:00
Gene S 9a855a7288 removing unused icons 2016-01-27 10:50:55 -08:00
Gene S d331367223 a few small fixes 2016-01-27 10:39:24 -08:00
Gene S 156f1f50bc visual improvements and fixes 2016-01-26 19:16:39 -08:00
Gene S 3acd66e5b3 removed bootstrap dependencies 2016-01-26 17:27:29 -08:00
or-else 876a7c1b1f removed bootstrap 2016-01-24 22:23:28 -08:00
or-else 444d7c006d lots of refactoring to remove bootstrap, still unfinished 2016-01-24 22:13:50 -08:00
Gene S 10353c9a1d refactoring message view (incomplete) 2016-01-22 18:00:56 -08:00
Gene S 22e2be2dc8 online presence 2016-01-20 19:07:36 -08:00
Gene S d3e2436527 Merge branch 'master' of https://github.com/tinode/example-react-js 2016-01-20 10:24:30 -08:00
Gene S 984f255d8a debigging 2016-01-20 10:24:24 -08:00
or-else 3f508da8e6 lots of refactoring to support read/received counts 2016-01-18 21:38:53 -08:00
or-else 295ffd640b lots of refactoring to support read/received counts 2016-01-17 15:59:07 -08:00
Gene S b344f9d7e4 Merge branch 'master' of https://github.com/tinode/example-react-js 2016-01-15 16:46:50 -08:00
Gene S ca49c83073 account creation works now 2016-01-15 16:46:24 -08:00
or-else d28d74a77a lass padding in contact list 2016-01-14 20:51:27 -08:00
Gene S 1b52fde005 README update 2016-01-14 19:10:14 -08:00
Gene S 31d5320922 bug fixes and typing notifications 2016-01-14 17:12:03 -08:00
Gene S e871dad863 now fetching and rendering messages 2016-01-13 17:58:41 -08:00
Gene S eee2db628a fetching messages from the server 2016-01-12 18:57:34 -08:00
or-else 4c759913c7 making contact list scrollable 2016-01-11 22:01:28 -08:00
or-else 4442cbae9d making contacts scrollable 2016-01-10 19:46:50 -08:00
or-else 6d0d75ab1d making contacts scrollable 2016-01-10 19:33:19 -08:00
or-else 3f2746cdb7 contacts loaded and shown 2016-01-10 16:01:44 -08:00
or-else d28c696190 continue to hook up server 2016-01-10 14:07:25 -08:00
or-else eba7510be8 adding subscription (incomplete) 2016-01-09 21:59:36 -08:00
Gene S d1442bfc93 hooked up to the server and added login error handling 2016-01-08 18:26:08 -08:00
Gene S 743d941204 hooked up the Tinode bindings, it calls the server now 2016-01-07 18:18:56 -08:00
Gene S f82bd966e0 made demo available online 2016-01-06 10:54:14 -08:00
Gene S aedc9854c1 made demo available online 2016-01-06 10:53:14 -08:00
Gene S 87c5b5bc6b graphic assets 2016-01-05 18:48:40 -08:00
Gene S 9f50eff10a moved all logic to AppView, added Sign Up view 2016-01-05 18:47:37 -08:00
Gene S 3b56e7ca2f moved files around for clarity 2016-01-05 11:07:55 -08:00
Gene S f64c8082ff daily commit 2016-01-04 19:12:31 -08:00
or-else 20be763dc1 daily commit 2016-01-03 20:49:32 -08:00
or-else 391a956af8 daily commit 2016-01-02 20:16:30 -08:00
or-else 2121a6a4f2 daily commit 2016-01-01 21:36:34 -08:00
Gene Sokolov dc8f82961a daily commit 2016-01-01 20:48:38 -08:00
Gene S d9015804c8 current status 2015-12-31 15:10:26 -08:00
Gene S 135bec17a8 Merge branch 'master' of https://github.com/tinode/react-js 2015-12-31 15:08:24 -08:00
Gene S 35624786ab Initial commit 2015-12-31 15:07:38 -08:00
Gene S 0688a360c7 initial pass at a react.js chat 2015-12-31 15:03:22 -08:00
211 changed files with 88600 additions and 139 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"comments": false,
"plugins": [
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"ast": true
}
]
]
}
+3
View File
@@ -0,0 +1,3 @@
umd/* binary
package-lock.json binary
*.min.css binary
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve TinodeWeb
title: ''
labels: bug
assignees: ''
---
If you want to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Mac OS, Windows 10 Home]
- Browser [e.g. Chrome, Safari]
- TinodeWeb Version [e.g. 0.16.6]
**Console log**
Please attach or insert console log which illustrates the problem. Please use the log produced by `index-dev.html`, not `index.html`; `index.html` uses minified javascript which produces unusable logs.
**Additional context**
Add any other context about the problem here.
+23
View File
@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
If you want to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+4 -138
View File
@@ -1,138 +1,4 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
node_modules
src/i18n/srci/
webapp.code-workspace.code-workspace
+25
View File
@@ -0,0 +1,25 @@
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "co.tinode.tindroidx",
"sha256_cert_fingerprints":
["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"]
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "web",
"site": "https://web.tinode.co"
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "co.tinode.tindroidx",
"sha256_cert_fingerprints":
["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"]
}
}]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+83 -1
View File
@@ -1,2 +1,84 @@
# im_webapp
# Tinode Web
## Briefing
Single-page web chat application for [Tinode](https://github.com/tinode/chat/). The app is built on
[React](https://reactjs.org/). The [Tinode javascript SDK](https://github.com/tinode/tinode-js/) has no external dependencies.
Overall, it's a lot like open source WhatsApp or Telegram web apps.
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist, some features are missing. The app was tested in the latest Chrome & Firefox only. NPM package is [available](https://www.npmjs.com/package/tinode-webapp).
Try possibly newer or older version live at https://sandbox.tinode.co/.
<a href="https://web.tinode.co/"><img src="web-app.svg" height=36></a>
For demo access and other instructions [see here](https://github.com/tinode/chat/#demosandbox).
## Installing and running
This is _NOT_ a standalone app, this is just a frontend, a client. It requires a [backend](https://github.com/tinode/chat/). See [installation instructions](https://github.com/tinode/chat/blob/master/INSTALL.md). See instructions for [enabling push notifications](push.md).
## Getting support
* Read [client-side](http://tinode.github.io/js-api/) and [server-side](https://github.com/tinode/chat/blob/master/docs/API.md) API documentation.
* For support, general questions, discussions post to [https://groups.google.com/d/forum/tinode](https://groups.google.com/d/forum/tinode).
* For bugs and feature requests [open an issue](https://github.com/tinode/webchat/issues/new).
* Use https://tinode.co/contact for commercial inquiries.
## Helping out
* If you appreciate our work, please help spread the word! Sharing on Reddit, HN, and other communities helps more than you think.
* Consider buying paid support: https://tinode.co/support.html
* If you are a software developer, send us your pull requests with bug fixes and new features.
* If you use the app and discover bugs or missing features, let us know by filing bug reports and feature requests. Vote for existing [feature requests](https://github.com/tinode/chat/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A%22feature+request%22) you find most valuable.
* If you speak a language other than English, [translate](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp) the apps into your language. You may also review and improve existing translations.
* If you are a UI/UX expert, help us polish the app UI.
* Use it: install it for your colleagues or friends at work or at home.
## Internationalization
The app is fully internationalized using [React-Intl](https://github.com/formatjs/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `#?hl=XX` parameter to the URL when starting the app, e.g. https://web.tinode.co/#?hl=ru.
The app is currently available in the following languages:
* English
* Chinese (simplified)
* Chinese (traditional)
* French
* German
* Korean
* Romanian
* Russian
* Spanish
More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp).
## Not done yet
* End-to-End encryption.
* Emoji support is weak.
* Mentions, hashtags.
* Previews not generated for links and docs.
* Location and contact sharing.
* Recording video messages.
## Other
* Demo avatars and some other graphics are from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
* Background patterns from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
* [Fonts](https://fonts.google.com/) and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web) from Google under [license](https://developers.google.com/terms) which permits commercial and non-commercial use.
* Push notifications built with https://firebase.google.com/.
## Responsive design
### Desktop screenshot
<p align="center">
<img src="web-desktop.jpg" alt="Desktop web: full app" width=810 />
</p>
### Mobile screenshots
<p align="center">
<kbd><img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /></kbd> <kbd><img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /></kbd> <kbd><img src="web-mob-info.png" alt="Mobile web: topic info" width=250 /></kbd> <kbd><img src="web-mob-forward.png" alt="Mobile web: forward message" width=250 /></kbd> <kbd><img src="web-mob-video-call.png" alt="Mobile web: video call" width=250 /></kbd> <kbd><img src="web-mob-avatar.png" alt="Mobile web: change avatar" width=250 /></kbd>
</p>
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+3241
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
// Get these values for your project from the https://console.firebase.google.com/
const FIREBASE_INIT = {
// Set up a web application in console then get the config object.
// The following keys are copied from the object:
apiKey: "AIzaSyD6X4ULR-RUsobvs1zZ2bHdJuPz39q2tbQ",
messagingSenderId: "114126160546",
projectId: "tinode-1000",
appId: "1:114126160546:web:aca6ea2981feb81fb44dfb",
// Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates.
// This value IS NOT included in the generated js config you get from
// the firebase console. It needs to be added separately.
messagingVapidKey: "BOgQVPOMzIMXUpsYGpbVkZoEBc0ifKY_f2kSU5DNDGYI6i6CoKqqxDd7w7PJ3FaGRBgVGJffldETumOx831jl58",
// Google analytics tag. Uncomment and use one you got from https://analytics.google.com/.
// measurementId: "G-ABCDEF1234"
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="192" height="192"><defs><filter id="f" color-interpolation-filters="sRGB"><feFlood flood-opacity=".251" flood-color="#000" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-1" result="offset"/><feComposite in="SourceGraphic" in2="offset" result="fbSourceGraphic"/><feColorMatrix result="fbSourceGraphicAlpha" in="fbSourceGraphic" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/><feFlood flood-opacity=".251" flood-color="#000" result="flood"/><feComposite in2="fbSourceGraphic" in="flood" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-3" result="offset"/><feComposite in2="offset" in="fbSourceGraphic" result="composite2"/></filter><filter id="d" color-interpolation-filters="sRGB"><feFlood flood-opacity=".196" flood-color="#FFF" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="out" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="2" result="offset"/><feComposite in="offset" in2="SourceGraphic" operator="atop" result="composite2"/></filter><filter id="b" color-interpolation-filters="sRGB"><feFlood flood-opacity=".196" flood-color="#000" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="out" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-2" result="offset"/><feComposite in="offset" in2="SourceGraphic" operator="atop" result="composite2"/></filter><filter id="c" color-interpolation-filters="sRGB"><feFlood flood-opacity=".196" flood-color="#000" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="out" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-2" result="offset"/><feComposite in="offset" in2="SourceGraphic" operator="atop" result="composite2"/></filter><linearGradient id="a"><stop offset="0" stop-opacity=".251"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient xlink:href="#a" id="e" x1="102.814" y1="97.076" x2="219.019" y2="170.481" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.29306 0 0 1.00596 -43.959 .807)"/></defs><g fill-rule="evenodd"><path d="M14.102 64A88 87.925 0 008 95.924c-.012 44.455 18.485 70.138 1.863 79.822-12.733 7.419 43.153 8.258 70.137 8.248V64H14.102z" fill="#fc5d3e" filter="url(#b)"/><path d="M80 64v119.994c8.24-.003 14.055-.08 14.055-.08L96 183.85a88 87.925 0 0088-87.926A88 87.925 0 00177.99 64H80z" fill="#45ae49" filter="url(#c)"/><path d="M96 8A88 87.925 0 009.52 80h172.978A88 87.925 0 0096 8z" fill="#2d526b" filter="url(#d)"/></g><path d="M88.002 40.975l-8.6 112.808 39.791 26.94c34.169-9.369 59.381-38.298 63.971-73.4z" fill="url(#e)" fill-rule="evenodd" opacity=".5"/><text y="153.195" x="53.125" style="line-height:125%;-inkscape-font-specification:'Arial, Bold';text-align:start" font-weight="700" font-size="160" font-family="Arial" letter-spacing="0" word-spacing="0"><tspan style="line-height:125%;-inkscape-font-specification:'Arial, Bold';text-align:start" y="153.195" x="53.125" fill="#fff">t</tspan></text></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+51
View File
@@ -0,0 +1,51 @@
<!-- DEVELOPMENT version of the web app. -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tinode</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="Tinode Web, version for developers" />
<meta name="description" content="Tinode instant messenger, development version" />
<meta name="application-url" content="https://web.tinode.co/">
<!-- Browser styling for Chrome, FF, Opera -->
<meta name="theme-color" content="#3949AB" />
<!-- Ask Google not to offer page translation -->
<meta name="google" value="notranslate" />
<!-- Open Graph description -->
<meta name="og:url" content="https://web.tinode.co/">
<meta name="og:title" content="Tinode Web Development">
<meta name="og:image" content="img/og-logo.jpeg">
<meta name="og:description" content="Tinode instant messenger, development version" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
<link rel="canonical" href="https://web.tinode.co/" />
<!-- Google's Roboto font -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<!-- Google's material design icons -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- ReactJS -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js"></script>
<!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6/react-intl.iife.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
<div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser.
TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.
</div>
</div></noscript>
<div id="mountPoint"></div>
<script src="firebase-init.js"></script>
<script src="umd/tinode.dev.js"></script>
<script src="umd/index.dev.js"></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>Tinode</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="Tinode Web" />
<meta name="description" content="Tinode instant messenger in a browser" />
<meta name="application-url" content="https://web.tinode.co/">
<meta name="theme-color" content="#3949AB" />
<meta name="google" value="notranslate" />
<meta name="format-detection" content="telephone=no" />
<meta name="og:url" content="https://web.tinode.co/">
<meta name="og:title" content="Tinode Web">
<meta name="og:image" content="img/og-logo.jpeg">
<meta name="og:description" content="Tinode instant messenger in a browser" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
<link rel="canonical" href="https://web.tinode.co/" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6.3.2/react-intl.iife.min.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
<div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser.
TinodeWeb does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.</div></div></noscript>
<div id="mountPoint"></div>
<script src="firebase-init.js"></script>
<script src="umd/tinode.prod.js"></script>
<script src="umd/index.prod.js"></script>
</body>
</html>
+45
View File
@@ -0,0 +1,45 @@
{
"name": "Tinode",
"short_name": "Tinode",
"description": "Tinode Web App.\nSee more at https://tinode.co/",
"categories": ["chat", "communication", "productivity"],
"icons": [
{
"src":"img/logo96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src":"img/logo192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "img/logo.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"start_url": "/",
"background_color": "silver",
"theme_color": "#3949AB",
"display": "standalone",
"gcm_sender_id": "103953800507",
"prefer_related_applications": true,
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=co.tinode.tindroidx",
"id": "co.tinode.tindroidx"
},
{
"platform": "itunes",
"url": "https://apps.apple.com/us/app/tinode/id1483763538"
}
],
"permissions": {
"audio-capture": {
"description": "Required for recording voice messages"
}
}
}
+10068
View File
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.23.0-rc1",
"repository": {
"type": "git",
"url": "https://github.com/tinode/webapp.git"
},
"scripts": {
"start": "echo 'Read installation instructions'",
"build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev",
"clean": "rm umd/* && rm css/base.min.css && rm src/i18n.min/*",
"build:css": "postcss css/base.css > css/base.min.css",
"build:dev": "webpack --mode development",
"build:prod": "webpack --mode production",
"build:i18n": "node scripts/i18n.js",
"i18n:missing": "node scripts/i18n.js missing",
"i18n:obsolete": "node scripts/i18n.js obsolete",
"vers": "node scripts/gen-version.js",
"extract": "formatjs extract \"src/**/*.{js,jsx}\" --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"compile": "formatjs compile"
},
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
"files": [
"umd/*.js",
"umd/*.map",
"audio/*.m4a",
"css/base.css",
"css/base.min.css",
"img/*.png",
"img/*.svg",
"img/*.jpeg",
"index-dev.html",
"index.html",
"manifest.json",
"service-worker.js",
"firebase-init.js",
"version.js"
],
"keywords": [
"chat",
"instant-messenger",
"message",
"messenger",
"react",
"web"
],
"locales": [
"de",
"en",
"es",
"fr",
"ko",
"ro",
"ru",
"zh",
"zh-TW"
],
"main": "umd/index.prod.js",
"bugs": {
"url": "https://github.com/tinode/webapp/issues"
},
"homepage": "https://github.com/tinode/chat",
"email": "info@tinode.co",
"author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0",
"dependencies": {
"firebase": "^9.17.2",
"libphonenumber-js": "^1.10.24",
"npm-check-updates": "^16.7.12",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^6.3.0",
"tinode-sdk": "^0.23.0-rc1",
"webm-duration-fix": "^1.0.4"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@formatjs/cli": "^6.0.4",
"babel-loader": "^9.1.2",
"babel-plugin-formatjs": "^10.4.0",
"browserslist": "^4.21.5",
"copy-webpack-plugin": "^11.0.0",
"cssnano": "^5.1.15",
"glob": "^9.3.0",
"postcss": "^8.4.21",
"postcss-cli": "^10.1.0",
"webpack": "^5.76.1",
"webpack-cli": "^5.0.1"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('cssnano')({
preset: 'default',
}),
],
};
+11
View File
@@ -0,0 +1,11 @@
# Push notifications
If you want to use the app with your own server and want web push notification to work you have to set them up:
* Register at https://firebase.google.com/, set up the project if you have not done so already.
* Follow instructions to create a web application https://support.google.com/firebase/answer/9326094 in your project.
* Follow instructions at https://support.google.com/firebase/answer/7015592 to get a Firebase configuration object ("Firebase SDK snippet").
* Locate `firebase-init.js` in the root folder of your copy of TinodeWeb app. Copy-paste the following keys from the configuration object to `firebase-init.js`: `apiKey`, `messagingSenderId`, `projectId`, `appId` (you may copy all keys).
* Copy `Web Push certificate` (Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates) to `messagingVapidKey` field in `firebase-init.js`.
* Double check that `firebase-init.js` contains the following keys: `apiKey`, `messagingSenderId`, `projectId`, `appId`, `messagingVapidKey`. The file may contain other optional keys.
* Copy Google-provided server key to `tinode.conf`, see details [here](https://github.com/tinode/chat/blob/master/docs/faq.md#q-how-to-setup-fcm-push-notifications).
+18
View File
@@ -0,0 +1,18 @@
const fs = require('fs');
const version = require('../package.json').version;
function writeVersion(fileName, useExport) {
fs.writeFile(fileName,
'// This is a generated file. Don\'t edit.\n\n'+
`${useExport ? 'export ' : ''}const PACKAGE_VERSION = "${version}";\n`,
{ flag: 'w' },
(err) => {
if (err) {
return console.log(err);
}
}
);
}
writeVersion('./src/version.js', true);
writeVersion('./version.js', false);
+127
View File
@@ -0,0 +1,127 @@
// 1. Updates translations files with new messages, mark no longer needed translations as obsolete.
// 2. Takes all translations and groups them into a single file.
// 3. When called with parameters 'missing' or 'obsolete' writes missing and obsolete translations to console.
const fs = require('fs');
const path = require('path');
const globSync = require('glob').sync;
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
const BASE_LANG = 'en';
const LANG_PATTERN = './src/i18n/*.json';
const LANG_PATTERN_ONE = './src/i18n/%s.json';
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
const args = process.argv.slice(2);
function getLocaleFromFileName(filename) {
const parts = filename.split('/');
return parts[parts.length - 1].split('.json')[0];
}
function printf(str) {
const args = [].slice.call(arguments, 1);
let i = 0;
return str.replace(/%s/g, () => args[i++]);
}
// Called with one or two parameters?
if (args[0] == 'missing' || args[0] == 'obsolete') {
// Extract missing or obsolete translations as requested.
// Read either all or just one translation.
const pattern = args[1] ? printf(LANG_PATTERN_ONE, args[1]) : LANG_PATTERN;
const extracted = globSync(pattern)
.map((filename) => {
const locale = getLocaleFromFileName(filename);
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
const dest = {};
// Copy messages while skipping missing and obsolete ones.
for (let key in translated) {
if ((args[0] == 'missing' && translated[key].missing) ||
(args[0] == 'obsolete' && translated[key].obsolete)) {
dest[key] = translated[key];
}
}
return { [locale]: dest };
})
.reduce((collection, descriptors) => {
return { ...collection, ...descriptors };
}, {});
console.log(JSON.stringify(extracted, null, 2)+"\n");
return;
}
// The formatjs/cli saves extracted messages into a file {'message_id': {defaultMessage: 'default message text', description: '...'}, ...}.
// Rewrite them into a single file en.json {'message_id': 'default message text', }.
// Read master-set of messages in default language.
const baseMessages = JSON.parse(fs.readFileSync(EXTRACTED_STRINGS, 'utf8'));
// Find messages present in baseMessages but missing in translations and vice versa.
// Mark missing or obsolete messages as such.
globSync(LANG_PATTERN)
.map((filename) => {
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
const locale = getLocaleFromFileName(filename);
// Refresh existing entries with possible new descriptions and default messages, add missing.
Object.entries(baseMessages).forEach((ent) => {
const [key, value] = ent;
let translation = translated[key] ? translated[key].translation : "";
if (!translation && locale == BASE_LANG) {
translation = value.defaultMessage;
}
translated[key] = {
translation: translation,
defaultMessage: value.defaultMessage,
description: value.description,
missing: !translation,
obsolete: false
};
});
// Find no longer needed messages.
Object.entries(translated).forEach((ent) => {
const [key, value] = ent;
if (!baseMessages[key] && translated[key]) {
// Obsolete, no longer missing.
translated[key].missing = !translated[key].translation;
translated[key].obsolete = true;
}
});
fs.writeFileSync(filename, JSON.stringify(translated, null, 2)+"\n");
});
// Legacy: Combine all translations into a single json {"lang_1": {...}, "lang_2: {...}"}
// while removing messages marked as missing or obsolete.
// New way: Emit one file per language with all unnecessary data stripped.
// Read all translations.
const messages = globSync(LANG_PATTERN)
.map((filename) => {
const locale = getLocaleFromFileName(filename);
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
const dest = {};
// Copy messages while skipping missing and obsolete ones.
for (let key in translated) {
if (!translated[key].missing && !translated[key].obsolete) {
dest[key] = translated[key].translation;
}
}
return { [locale]: dest };
})
.reduce((collection, descriptors) => {
return { ...collection, ...descriptors };
}, {});
// Write minified translation files, one per language.
for (let lang in messages) {
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
}
+180
View File
@@ -0,0 +1,180 @@
// Must be located at the root.
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-app-compat.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-messaging-compat.js');
importScripts('firebase-init.js');
importScripts('version.js');
// Channel to notify the webapp. There is no BroadcastChannel in Safari.
const webAppChannel = (typeof BroadcastChannel == 'function') ?
new BroadcastChannel('tinode-sw') : null;
// Basic internationalization.
const i18n = {
'de': {
'new_message': "Neue Nachricht",
'new_chat': "Neuer Chat",
},
'en': {
'new_message': "New message",
'new_chat': "New chat",
},
'fr': {
'new_message': "Nouveau message",
'new_chat': "Nouvelle conversation",
},
'es': {
'new_message': "Nuevo mensaje",
'new_chat': "Nueva conversación",
},
'ko': {
'new_message': "새로운 메시지",
'new_chat': "새로운 채팅",
},
'ro': {
'new_message': "Mesaj nou",
'new_chat': "Chat nou",
},
'ru': {
'new_message': "Новое сообщение",
'new_chat': "Новый чат",
},
'zh': {
'new_message': "新讯息",
'new_chat': "新聊天",
},
'zh-TW': {
'new_message': "新訊息",
'new_chat': "新聊天",
}
};
self.i18nMessage = function(id) {
if (!id) {
return null;
}
// Choose translations: given something like 'de-CH', try 'de-CH' then 'de' then 'en'.
const lang = i18n[self.locale] || i18n[self.baseLocale] || i18n['en'];
// Try finding string by id in the specified language, if missing try English, otherwise use the id itself
// as the last resort.
return lang[id] || i18n['en'][id] || id;
}
firebase.initializeApp(FIREBASE_INIT);
const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage(payload => {
// Notify webapp that a message was received.
if (webAppChannel) {
webAppChannel.postMessage(payload.data);
}
if (payload.data.silent == 'true') {
// No need to show anything.
return;
}
const titles = {'msg': 'new_message', 'sub': 'new_chat'};
const pushType = payload.data.what || 'msg';
const title = payload.data.title || self.i18nMessage(titles[pushType]);
if (title) {
const options = {
body: payload.data.content || '', // TODO: content for 'sub' should be topic's or user's title.
icon: '/img/logo96.png', // TODO: use topic's or user's avatar (would have to fetch for 'sub', read from db for 'msg').
badge: '/img/badge96.png',
tag: payload.data.topic || undefined,
data: {
topic: payload.data.topic
}
};
return self.registration.showNotification(title, options);
}
});
// Update service worker immediately for both the current client
// and all other active clients.
self.addEventListener('install', _ => {
self.skipWaiting();
});
// This code handles a click on notification: takes
// the user to the browser tab with the chat or opens a new tab.
self.addEventListener('notificationclick', event => {
const data = event.notification.data;
event.notification.close();
if (!data) {
console.info("Missing 'data' in notification", event.notification);
return;
}
const urlHash = '#/' + data.topic;
event.waitUntil(self.clients.matchAll({
type: 'window',
includeUncontrolled: true})
.then(windowClients => {
let anyClient = null;
for (let i = 0; i < windowClients.length; i++) {
const url = new URL(windowClients[i].url);
if (url.hash.includes(data.topic)) {
// Found the Tinode tab with the right topic open.
return windowClients[i].focus();
} else {
// This will be the least recently used tab.
anyClient = windowClients[i];
}
}
// Found tab with Tinode on a different topic,
// navigate to the right topic.
if (anyClient) {
const url = new URL(anyClient.url);
url.hash = urlHash;
return anyClient.focus().then(thisClient => {
return thisClient.navigate(url);
});
}
// Did not find a Tinode browser tab. Open one.
const url = new URL(self.location.origin);
url.hash = urlHash;
return clients.openWindow(url);
}));
});
// This is needed for 'Add to Home Screen'.
self.addEventListener('fetch', event => {
if (event.request.method != 'GET') {
return;
}
event.respondWith((async _ => {
// Try to find the response in the cache.
const cache = await caches.open(PACKAGE_VERSION);
const reqUrl = new URL(event.request.url);
// Using ignoreSearch=true to read cached images and docs despite different auth signatures.
const cachedResponse = await cache.match(event.request, {ignoreSearch: (self.location.origin == reqUrl.origin)});
if (cachedResponse) {
return cachedResponse;
}
// Not found in cache.
const response = await fetch(event.request);
if (!response || response.status != 200 || response.type != 'basic') {
return response;
}
if (reqUrl.protocol == 'http:' || reqUrl.protocol == 'https:') {
await cache.put(event.request, response.clone());
}
return response;
})());
});
// This code gets the human language from the webapp.
self.addEventListener('message', event => {
const data = JSON.parse(event.data);
// The locale is used for selecting strings in an appropriate language.
self.locale = data.locale || '';
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
});
+124
View File
@@ -0,0 +1,124 @@
import { PACKAGE_VERSION } from './version.js';
// Name of this application, used in the User-Agent.
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.21');
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
// The array of possible hosts to connect to.
export const KNOWN_HOSTS = {hosted: 'web.tinode.co', local: 'localhost:6060'};
// Default host name and port to connect to.
export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
// Enable console logging.
export const LOGGING_ENABLED = true;
// Minimum time between two keypress notifications, milliseconds.
export const KEYPRESS_DELAY = 3_000;
// Delay before sending a read notification, milliseconds.
export const READ_DELAY = 1_500;
// The default shortest allowed tag length. Matches the value on the server.
export const MIN_TAG_LENGTH = 2;
// The default greatest allowed tag length. Matches the value on the server.
export const MAX_TAG_LENGTH = 96;
// The default maximum number of tags allowed. Matches the value on the server.
export const MAX_TAG_COUNT = 16;
// Access mode for P2P subscriptions initiated by the current user.
export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS';
// Access mode for new group topics created by the current user.
export const NEW_GRP_ACCESS_MODE = 'JRWPSAO';
// Access mode for a channel.
export const CHANNEL_ACCESS_MODE = 'JR';
// Access mode for no access.
export const NO_ACCESS_MODE = 'N';
// Mediaquery breakpoint between desktop and mobile, in px. Should match the value
// in @media (max-size: 640px) in base.css
export const MEDIA_BREAKPOINT = 640;
// Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px.
export const REM_SIZE = 13;
// Size of the avatar image: when image dimensions are greater or the image is not square, it's resized to
// a square of this size or less.
export const AVATAR_SIZE = 384;
// Maximum size of an avatar in bytes for sending in-band. Bigger avatars will be sent out of band (as uploads).
export const MAX_AVATAR_BYTES = 4096;
// Size of the broken_image shown in MessagesView
export const BROKEN_IMAGE_SIZE = 32;
// Size of video preview when dimensions are missing.
export const NO_DIMENSIONS_VIDEO = 128;
// Number of chat messages to fetch in one call.
export const MESSAGES_PAGE = 24;
// Default maximum in-band (included directly into the message) attachment size which fits into
// a message of 256K in size. Used when the server-provided value is unavailable. The actual
// binary size of the attachment should be smaller due to base64 encoding expansion and some overhead,
// for instance 1024 bytes.
// Increase this limit to a greater value in production, if desired. Also increase
// max_message_size in the server config.
export const MAX_INBAND_ATTACHMENT_SIZE = 262_144;
// Default absolute maximum attachment size to be used with the server = 8MB.
// Used when the server-provided value is unavailable. Increase to something like 100MB in production.
export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
// Maximum allowed linear dimension (pixels) of an image sent inline. Larger images will be shrunk
// to make the larger dimension fit under this size. You may want to adjust it to 1600 or
// 2400 for production.
export const MAX_IMAGE_DIM = 1024;
// Linear dimensions of image preview: shrink image under this size for previews.
export const IMAGE_PREVIEW_DIM = 64;
// Linear dimensions of video screencapture: shrink image under this size for previews.
export const VIDEO_PREVIEW_DIM = 96;
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
export const IMAGE_THUMBNAIL_DIM = 36;
// Width of video thumbnail: allow it to be wider, but not taller. Use IMAGE_THUMBNAIL_DIM for height.
export const VIDEO_THUMBNAIL_WIDTH = 48;
// Maximum number of online users to be shown in a topic title bar. Others will be
// hidden under "+X more"
export const MAX_ONLINE_IN_TOPIC = 4;
// Maximum length of user name, topic title, and private comment.
export const MAX_TITLE_LENGTH = 60;
// Maximum length of topic description.
export const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
// Maximum length of user name when displaying a video call.
export const MAX_PEER_TITLE_LENGTH = 20;
// Length of message previews in chat list, in characters.
export const MESSAGE_PREVIEW_LENGTH = 80;
// Length of a quote in quoted reply (for outgoing messages).
export const QUOTED_REPLY_LENGTH = 30;
// Length of a preview of a forwarded message.
export const FORWARDED_PREVIEW_LENGTH = 84;
// Length of preview for a message being edited.
export const EDIT_PREVIEW_LENGTH = 48;
// Minimum duration of an audio recording in milliseconds (2 sec).
export const MIN_DURATION = 2_000;
// Maximum duration of an audio recording in milliseconds (10 min).
export const MAX_DURATION = 600_000;
// Link for "Contact Us".
export const LINK_CONTACT_US = 'mailto:support@tinode.co';
// Link to Privacy Policy.
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
// Link to Terms of Service.
export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
+8
View File
@@ -0,0 +1,8 @@
// Video call states.
export const CALL_STATE_NONE = 0;
export const CALL_STATE_OUTGOING_INITATED = 1;
export const CALL_STATE_INCOMING_RECEIVED = 2;
export const CALL_STATE_IN_PROGRESS = 3;
// Video call message headers representing call state.
export const CALL_HEAD_STARTED = 'started';
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+939
View File
@@ -0,0 +1,939 @@
{
"action_block_contact": {
"defaultMessage": "Block Contact",
"description": "Flat button [Block Contact]"
},
"action_cancel": {
"defaultMessage": "cancel",
"description": "Call to action [cancel]"
},
"action_clear_messages": {
"defaultMessage": "Clear Messages",
"description": "Flat button [Clear Messages] (soft-delete messages)"
},
"action_delete_messages": {
"defaultMessage": "Clear Messages for All",
"description": "Flat button [Clear for All] (hard-delete all messages)"
},
"action_leave_chat": {
"defaultMessage": "Leave Conversation",
"description": "Flat button [Leave Conversation]"
},
"action_report_chat": {
"defaultMessage": "Report Conversation",
"description": "Flat button [Report Group]"
},
"already_in_call": {
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one"
},
"archived_contacts": {
"defaultMessage": "Archived contacts ({count})",
"description": "Label for archived chats"
},
"badge_danger": {
"defaultMessage": "Untrustworthy",
"description": "Suspicious or untrustworthy account or topic badge"
},
"badge_owner": {
"defaultMessage": "owner",
"description": "Badge for indicating the owner"
},
"badge_staff": {
"defaultMessage": "Staff-managed",
"description": "Explanation of a staff-managed account or topic badge"
},
"badge_verified": {
"defaultMessage": "Verified/official",
"description": "Explanation of a verified account or topic badge"
},
"badge_you": {
"defaultMessage": "you",
"description": "Badge for indicating the current user"
},
"block_contact_warning": {
"defaultMessage": "Are you sure you want to block this contact?",
"description": "Alert dialog warning when blocking a contact."
},
"blocked_contacts_link": {
"defaultMessage": "Blocked contacts ({count})",
"description": "Blocked contacts link"
},
"button_add_members": {
"defaultMessage": "Add members",
"description": "Flat button [Add members] (to topic)"
},
"button_cancel": {
"defaultMessage": "Cancel",
"description": "Button [Cancel]"
},
"button_confirm": {
"defaultMessage": "Confirm",
"description": "Button [Confirm]"
},
"button_create": {
"defaultMessage": "Create",
"description": "Button [Create]"
},
"button_delete_account": {
"defaultMessage": "Delete account",
"description": "Button [Delete account]"
},
"button_edit": {
"defaultMessage": "Edit",
"description": "Call to action [Edit]"
},
"button_logout": {
"defaultMessage": "Logout",
"description": "Button [Logout]"
},
"button_ok": {
"defaultMessage": "OK",
"description": "Button [OK]"
},
"button_reset": {
"defaultMessage": "Reset",
"description": "Button [Reset]"
},
"button_security": {
"defaultMessage": "Security",
"description": "Navigaton button for security panel."
},
"button_send_request": {
"defaultMessage": "Send request",
"description": "Button [Send request]"
},
"button_sign_in": {
"defaultMessage": "Sign in",
"description": "Button [Sign In]"
},
"button_sign_up": {
"defaultMessage": "Sign up",
"description": "Create account button [Sign Up]"
},
"button_subscribe": {
"defaultMessage": "Subscribe",
"description": "Button [Subscribe]"
},
"button_update": {
"defaultMessage": "Update",
"description": "Button [Update]"
},
"call_busy": {
"defaultMessage": "busy",
"description": "Label for busy call line"
},
"call_cancelled": {
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call"
},
"call_declined": {
"defaultMessage": "declined",
"description": "Label for declined call"
},
"call_disconnected": {
"defaultMessage": "disconnected",
"description": "Label for disconnected call"
},
"call_in_progress": {
"defaultMessage": "in progress",
"description": "Label for call in progress"
},
"call_missed": {
"defaultMessage": "missed",
"description": "Label for missed incoming call"
},
"calls_incoming": {
"defaultMessage": "Incoming call",
"description": "Incoming call label"
},
"calls_outgoing": {
"defaultMessage": "Outgoing call",
"description": "Outgoing call label"
},
"calls_you_label": {
"defaultMessage": "You",
"description": "Shown over the local video screen"
},
"cannot_initiate_file_upload": {
"defaultMessage": "Cannot initiate file upload.",
"description": "Generic error messagewhen attachment fails"
},
"change_email": {
"defaultMessage": "Change email",
"description": "Prompt to change current email"
},
"change_phone": {
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number"
},
"channel": {
"defaultMessage": "channel",
"description": "Subtitle shown for channels in MessagesView instead of last seen"
},
"channel_prompt": {
"defaultMessage": "This is a channel",
"description": "Checkbox label when creating a channel"
},
"chat_invitation": {
"defaultMessage": "You are invited to start a new chat. What would you like to do?",
"description": "New chat invitation message: [Accept] [Ignore] [Block]."
},
"chat_invitation_accept": {
"defaultMessage": "Accept",
"description": "Action [Accept] for chat invitation."
},
"chat_invitation_block": {
"defaultMessage": "Block",
"description": "Action [Block] for chat invitation."
},
"chat_invitation_ignore": {
"defaultMessage": "Ignore",
"description": "Action [Ignore] for chat invitation."
},
"clear_messages_warning": {
"defaultMessage": "Are you sure you want to clear all messages? It cannot be undone.",
"description": "Alert dialog warning when deleting all messages."
},
"code_doesnot_match": {
"defaultMessage": "Code does not match",
"description": "Error message when the credential validation code is incorrect."
},
"contacts_not_found": {
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found"
},
"contacts_not_found_short": {
"defaultMessage": "No contacts match ''{query}''",
"description": "Shown in ContactsView when search returned no results"
},
"cred_confirmed_successfully": {
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated."
},
"credential_email_prompt": {
"defaultMessage": "Your registration email",
"description": "Placeholder for entering email"
},
"current_email": {
"defaultMessage": "Current email",
"description": "Label for old email"
},
"current_phone": {
"defaultMessage": "Current phone number",
"description": "Label for old phone"
},
"delete_account": {
"defaultMessage": "Delete account",
"description": "Title for delete account warning"
},
"delete_account_warning": {
"defaultMessage": "Are you sure you want to delete your account? It cannot be undone.",
"description": "Warning message when deleting an account"
},
"delete_messages_warning": {
"defaultMessage": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
"description": "Alert dialog warning when hard-deleting all messages."
},
"description_editing_placeholder": {
"defaultMessage": "Description (optional)",
"description": "Placeholder for editing topic description"
},
"download_action": {
"defaultMessage": "download",
"description": "Call to action [download]"
},
"drafty_attachment": {
"defaultMessage": "Attachment",
"description": "Comment for attachment in Drafty"
},
"drafty_form": {
"defaultMessage": "Form:",
"description": "Comment for form in Drafty"
},
"drafty_image": {
"defaultMessage": "Picture",
"description": "Comment for embedded images in Drafty"
},
"drafty_unknown": {
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty"
},
"drafty_video": {
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty"
},
"drag_file": {
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner"
},
"editing_message": {
"defaultMessage": "Editing",
"description": "Title over message editing preview"
},
"email_dative": {
"defaultMessage": "email",
"description": "Dative case of 'email', i.e. 'email' in 'by email'"
},
"email_prompt": {
"defaultMessage": "Email, e.g. jdoe@example.com",
"description": "Input placeholder for email entry"
},
"enable_peers_messaging": {
"defaultMessage": "Enable",
"description": "Call to action to enable peer's messaging"
},
"enter_confirmation_code_prompt": {
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code"
},
"error_invalid_id": {
"defaultMessage": "Invalid ID",
"description": "Error message"
},
"file_attachment_too_large": {
"defaultMessage": "The file size {size} exceeds the {limit} limit.",
"description": "Error message when attachment is too large"
},
"forgot_password_link": {
"defaultMessage": "Forgot password?",
"description": "Link to Reset password form"
},
"forward_to": {
"defaultMessage": "Forward to"
},
"forward_to_search_placeholder": {
"defaultMessage": "Search contacts",
"description": "Contact search prompt when forwarding a message."
},
"full_name_prompt": {
"defaultMessage": "Full name, e.g. John Doe",
"description": "Input placeholder for person's full name"
},
"granted_permissions": {
"defaultMessage": "Granted",
"description": "Title for permissions"
},
"group_has_no_members": {
"defaultMessage": "No members",
"description": "Shown in place of group members"
},
"group_user_id_prompt": {
"defaultMessage": "Group or User ID",
"description": "Prompt for entering user or group ID"
},
"icon_title_add_image": {
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image"
},
"icon_title_attach_file": {
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file"
},
"icon_title_delete": {
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio"
},
"icon_title_pause": {
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback"
},
"icon_title_play": {
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback"
},
"icon_title_record_voice": {
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message"
},
"icon_title_resume": {
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback"
},
"icon_title_send": {
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message"
},
"image_caption_prompt": {
"defaultMessage": "Image caption",
"description": "Prompt in SendMessage for attached image"
},
"invalid_content": {
"defaultMessage": "invalid content",
"description": "Shown when the message is unreadable"
},
"invalid_security_token": {
"defaultMessage": "Invalid security token",
"description": "Error message when resetting password."
},
"label_client": {
"defaultMessage": "Client:",
"description": "Label for a client version"
},
"label_content_type": {
"defaultMessage": "Content type:",
"description": "Label for file content type (mime)"
},
"label_default_access_mode": {
"defaultMessage": "Default access mode:",
"description": "Label for default access mode"
},
"label_description": {
"defaultMessage": "Description",
"description": "Label for editing topic description"
},
"label_file_name": {
"defaultMessage": "File name:",
"description": "Label for a file name"
},
"label_group_members": {
"defaultMessage": "Group members:",
"description": "Section title or label"
},
"label_incognito_mode": {
"defaultMessage": "Incognito mode:",
"description": "Label for incognito mode toggle"
},
"label_message_sound": {
"defaultMessage": "Message sound:",
"description": "Label for message sounds toggle"
},
"label_muting_topic": {
"defaultMessage": "Muted:",
"description": "Label for Muting/unmuting the topic"
},
"label_new_password": {
"defaultMessage": "New password",
"description": "Label for entering a new password"
},
"label_other_user": {
"defaultMessage": "Other",
"description": "Label for the other user when the user is unnamed"
},
"label_password": {
"defaultMessage": "Password",
"description": "Label for password editing"
},
"label_permissions": {
"defaultMessage": "Permissions:",
"description": "Section title"
},
"label_private": {
"defaultMessage": "Private comment",
"description": "Label for editing 'private'"
},
"label_push_notifications": {
"defaultMessage": "Notification alerts:",
"description": "Label for push notifications switch"
},
"label_push_notifications_disabled": {
"defaultMessage": "Notification alerts (requires HTTPS):",
"description": "Label for push notifications switch"
},
"label_reset_password": {
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field"
},
"label_reset_password_tel": {
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone"
},
"label_sdk": {
"defaultMessage": "SDK:"
},
"label_server": {
"defaultMessage": "Server:",
"description": "Label for a server version"
},
"label_server_address": {
"defaultMessage": "Server address:"
},
"label_server_to_use": {
"defaultMessage": "Server to use:",
"description": "Label for server selector in SettingsView"
},
"label_size": {
"defaultMessage": "Size:",
"description": "Label for file size"
},
"label_topic_name": {
"defaultMessage": "Name",
"description": "Label for editing topic name"
},
"label_unarchive_topic": {
"defaultMessage": "Archived:",
"description": "Label for unarchiving the topic"
},
"label_use_secure_connection": {
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView"
},
"label_user_contacts": {
"defaultMessage": "Contacts",
"description": "Label for user contacts"
},
"label_user_id": {
"defaultMessage": "ID:",
"description": "Label for user address (ID)"
},
"label_wire_transport": {
"defaultMessage": "Wire transport:",
"description": "Label for wire transport selection in SettingsView"
},
"label_you": {
"defaultMessage": "You:",
"description": "Label for the current user"
},
"label_your_name": {
"defaultMessage": "Your name",
"description": "Label for full name editing"
},
"label_your_permissions": {
"defaultMessage": "Your permissions:",
"description": "Label for current user permissions"
},
"last_seen_timestamp": {
"defaultMessage": "Last seen",
"description": "Label for the timestamp of when the user or topic was last online"
},
"leave_chat_warning": {
"defaultMessage": "Are you sure you want to leave this conversation?",
"description": "Alert dialog warning when unsubscribing from a chat."
},
"link_contact_us": {
"defaultMessage": "Contact Us",
"description": "Ancor text for contacting us by email"
},
"link_privacy_policy": {
"defaultMessage": "Privacy Policy",
"description": "Ancor text for privacy policy link"
},
"link_terms_of_service": {
"defaultMessage": "Terms of Service",
"description": "Ancor text for terms of service link"
},
"loading_note": {
"defaultMessage": "Loading...",
"description": "Message shown when component is loading"
},
"login_prompt": {
"defaultMessage": "Login",
"description": "Placeholer for username/login"
},
"menu_item_archive_topic": {
"defaultMessage": "Archive",
"description": "Move topic from the list of active chats to archive"
},
"menu_item_audio_call": {
"defaultMessage": "Call",
"description": "Start audio call"
},
"menu_item_block": {
"defaultMessage": "Block",
"description": "Block topic or user"
},
"menu_item_clear_messages": {
"defaultMessage": "Clear messages",
"description": "Delete all messages"
},
"menu_item_clear_messages_for_all": {
"defaultMessage": "Clear for All",
"description": "Delete all message(s) for all members"
},
"menu_item_delete": {
"defaultMessage": "Delete",
"description": "Delete selected messages"
},
"menu_item_delete_for_all": {
"defaultMessage": "Delete for All",
"description": "Delete selected message(s) for all members"
},
"menu_item_delete_topic": {
"defaultMessage": "Delete",
"description": "Delete entire topic"
},
"menu_item_edit": {
"defaultMessage": "Edit",
"description": "Edit message"
},
"menu_item_edit_permissions": {
"defaultMessage": "Edit permissions",
"description": "Menu item [Edit permissions]"
},
"menu_item_forward": {
"defaultMessage": "Forward",
"description": "Forward message"
},
"menu_item_info": {
"defaultMessage": "Info",
"description": "Show extended topic information"
},
"menu_item_member_delete": {
"defaultMessage": "Remove",
"description": "Remove user from topic"
},
"menu_item_mute": {
"defaultMessage": "Mute",
"description": "Turn off notifications"
},
"menu_item_reply": {
"defaultMessage": "Reply",
"description": "Reply to message"
},
"menu_item_restore_topic": {
"defaultMessage": "Restore",
"description": "Restore topic from archive"
},
"menu_item_send_retry": {
"defaultMessage": "Retry",
"description": "Retry sending message"
},
"menu_item_unblock": {
"defaultMessage": "Unblock",
"description": "Unblock topic or user"
},
"menu_item_unmute": {
"defaultMessage": "Unmute",
"description": "Turn notifications on"
},
"menu_item_video_call": {
"defaultMessage": "Video call",
"description": "Start video call"
},
"message_edited_marker": {
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited"
},
"message_sending": {
"defaultMessage": "sending...",
"description": "Message being sent, in place of time stamp"
},
"message_sending_failed": {
"defaultMessage": "failed",
"description": "Failed to send message, in place of time stamp"
},
"messages_not_readable": {
"defaultMessage": "no access to messages",
"description": "Message shown in topic without the read access"
},
"messaging_disabled_prompt": {
"defaultMessage": "Messaging disabled",
"description": "Prompt in SendMessage in read-only topic"
},
"mobile_number_required": {
"defaultMessage": "Mobile phone number required",
"description": "Error message"
},
"mobile_phone_number": {
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number"
},
"more_online_members": {
"defaultMessage": "+{overflow} more",
"description": "Shown in MessagesView title bar when the number of online subscribers exceeds MAX_ONLINE_IN_TOPIC"
},
"new_email": {
"defaultMessage": "New email",
"description": "Prompt for entering a new email"
},
"new_message_prompt": {
"defaultMessage": "New message",
"description": "Prompt in send message field"
},
"new_password_placeholder": {
"defaultMessage": "Enter new password",
"description": "Placeholder for entering new password"
},
"new_phone_number": {
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number"
},
"no_connection": {
"defaultMessage": "No connection",
"description": "Warning that the user is offline."
},
"no_contacts": {
"defaultMessage": "You have no contacts :-(",
"description": "Shown in ContactsView when the user has no contacts"
},
"numeric_confirmation_code_prompt": {
"defaultMessage": "Numbers only",
"description": "Prompt for numeric conformation code"
},
"online_now": {
"defaultMessage": "online now",
"description": "Indicator that the user or topic is currently online"
},
"panel_title_crop": {
"defaultMessage": "Drag to Adjust",
"description": "Title for AvatarCropView."
},
"panel_title_general": {
"defaultMessage": "General",
"description": "Title for TopicCommon."
},
"panel_title_info": {
"defaultMessage": "Info",
"description": "Title for InfoView"
},
"panel_title_members": {
"defaultMessage": "Members",
"description": "Title for managing group members view."
},
"panel_title_security": {
"defaultMessage": "Security",
"description": "Title for TopicSecirity and AccSecurity."
},
"password_i_have_code": {
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code"
},
"password_prompt": {
"defaultMessage": "Password",
"description": "Placeholder/prompt for entering password"
},
"password_reset_email_sent": {
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent"
},
"password_reset_sms_sent": {
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent"
},
"password_unchanged_prompt": {
"defaultMessage": "Unchanged",
"description": "Message in editor while password is unchanged"
},
"peers_messaging_disabled": {
"defaultMessage": "Peer's messaging is disabled.",
"description": "Shown when the p2p peer's messaging is disabled"
},
"permission_admin": {
"defaultMessage": "Approve ({val})",
"description": "Name of A permission"
},
"permission_delete": {
"defaultMessage": "Delete ({val})",
"description": "Name of D permission"
},
"permission_join": {
"defaultMessage": "Join ({val})",
"description": "Name of J permission"
},
"permission_owner": {
"defaultMessage": "Owner ({val})",
"description": "Name of O permission"
},
"permission_pres": {
"defaultMessage": "Get notified ({val})",
"description": "Name of P permission"
},
"permission_read": {
"defaultMessage": "Read ({val})",
"description": "Name of R permission"
},
"permission_share": {
"defaultMessage": "Share ({val})",
"description": "Name of S permission"
},
"permission_write": {
"defaultMessage": "Write ({val})",
"description": "Name of W permission"
},
"permissions_anonymous": {
"defaultMessage": "Anonymous",
"description": "Title for editing default anonymous permissions"
},
"permissions_authenticated": {
"defaultMessage": "Authenticated",
"description": "Title for editing default authenticated permissions"
},
"permissions_user": {
"defaultMessage": "User's Permissions",
"description": "Title for editing user's permissions"
},
"phone_dative": {
"defaultMessage": "phone",
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
},
"pin_message": {
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top."
},
"private_editing_placeholder": {
"defaultMessage": "Visible to you only",
"description": "Placeholder for editing 'private'"
},
"push_init_failed": {
"defaultMessage": "Failed to initialize push notifications",
"description": "Error message when push notifications have failed to initialize."
},
"reconnect_countdown": {
"defaultMessage": "Disconnected. Reconnecting in {seconds}…",
"description": "Message shown when an app update is available."
},
"reconnect_now": {
"defaultMessage": "Try now",
"description": "Prompt for reconnecting now"
},
"reload_update": {
"defaultMessage": "Reload",
"description": "Call to action to reload application when update is available."
},
"report_chat_warning": {
"defaultMessage": "Are you sure you want to block and report this conversation?",
"description": "Alert dialog warning when reporting a conversation for abuse"
},
"requested_permissions": {
"defaultMessage": "Requested",
"description": "Title for permissions"
},
"save_attachment": {
"defaultMessage": "save",
"description": "Call to save an attachment"
},
"search_for_contacts": {
"defaultMessage": "Use search to find contacts",
"description": "Text shown in contacts view when user entered no search query."
},
"search_no_results": {
"defaultMessage": "Search returned no results",
"description": "Text shown in contacts view when query returned no results."
},
"search_placeholder": {
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"description": "Placeholder in contacts search field"
},
"sidepanel_title_acc_notifications": {
"defaultMessage": "Notifications",
"description": "Sidepanel title for AccNotificationsView."
},
"sidepanel_title_acc_support": {
"defaultMessage": "Support",
"description": "Sidepanel title for AccSupportView."
},
"sidepanel_title_account_settings": {
"defaultMessage": "Account Settings",
"description": "Sidepanel title for AccountSettingsView."
},
"sidepanel_title_archive": {
"defaultMessage": "Archived Chats",
"description": "Sidepanel title for ContactsView-Archive."
},
"sidepanel_title_blocked": {
"defaultMessage": "Blocked Chats",
"description": "Sidepanel title for ContactsView-Blocked."
},
"sidepanel_title_cred": {
"defaultMessage": "Confirm Credentials",
"description": "Sidepanel title for ValidationView."
},
"sidepanel_title_login": {
"defaultMessage": "Sign In",
"description": "Sidepanel title for LoginView."
},
"sidepanel_title_newtpk": {
"defaultMessage": "Start New Chat",
"description": "Sidepanel title for NewTopicView."
},
"sidepanel_title_register": {
"defaultMessage": "Create Account",
"description": "Sidepanel title for CreateAccountView."
},
"sidepanel_title_reset": {
"defaultMessage": "Reset Password",
"description": "Sidepanel title for PasswordResetView."
},
"sidepanel_title_settings": {
"defaultMessage": "Settings",
"description": "Sidepanel title for SettingsView."
},
"stay_logged_in": {
"defaultMessage": "Stay logged in",
"description": "Label for a checkbox"
},
"tabtitle_find_user": {
"defaultMessage": "find",
"description": "Tab title Find"
},
"tabtitle_group_by_id": {
"defaultMessage": "by id",
"description": "Tab title Find topic by ID"
},
"tabtitle_new_group": {
"defaultMessage": "new group",
"description": "Tab title New Group"
},
"tags_editor_no_tags": {
"defaultMessage": "Add some tags",
"description": "Tag editor prompt when no tags are found."
},
"tags_not_found": {
"defaultMessage": "No tags defined. Add some.",
"description": ""
},
"title_all_contacts": {
"defaultMessage": "All Contacts",
"description": "Section title [All Contacts]"
},
"title_group_members": {
"defaultMessage": "Group Members",
"description": "Section title"
},
"title_manage_tags": {
"defaultMessage": "Manage",
"description": "Section title for the list of tags"
},
"title_not_found": {
"defaultMessage": "Not found",
"description": "Title shown when topic is not found"
},
"title_permissions": {
"defaultMessage": "Permissions",
"description": "Section title"
},
"title_tag_manager": {
"defaultMessage": "Tags (search & discovery)",
"description": "Section title for TagManager"
},
"topic_block_warning": {
"defaultMessage": "Are you sure you want to block this conversation?",
"description": "Alert warning when blocking a topic."
},
"topic_delete": {
"defaultMessage": "Delete Conversation",
"description": "Alert title when deleting the topic."
},
"topic_delete_warning": {
"defaultMessage": "Are you sure you want to delete this conversation? It cannot be undone.",
"description": "Alert warning when deleting entire topic"
},
"topic_name_editing_placeholder": {
"defaultMessage": "Freeform name of the group",
"description": "Prompt for entering topic name"
},
"unnamed_topic": {
"defaultMessage": "Unnamed",
"description": "Title shown when the topic has no name"
},
"unpin_message": {
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message."
},
"unrecognized_video_format": {
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid"
},
"update_available": {
"defaultMessage": "Update available.",
"description": "Message shown when an app update is available."
},
"upload_finishing": {
"defaultMessage": "finishing...",
"description": "Notification that upload is finishing"
},
"user_not_found": {
"defaultMessage": "Not found",
"description": "In place of a user's full name when the user is not found."
}
}
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+1665
View File
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
// Put all packages together.
// Used to generate umd/index.prod.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import { IntlProvider } from 'react-intl';
import TinodeWeb from './views/tinode-web.jsx';
import HashNavigation from './lib/navigation.js';
// Insert google analytics script and tag if configured.
if (typeof FIREBASE_INIT != 'undefined' && FIREBASE_INIT && FIREBASE_INIT.measurementId) {
const head = document.getElementsByTagName('head')[0];
let script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + FIREBASE_INIT.measurementId;
script.async = true;
head.prepend(script);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', FIREBASE_INIT.measurementId);
}
// Allow loading translation strings for just one language.
const messageLoader = {
'de': _ => import('./i18n.min/de.json'),
'en': _ => import('./i18n.min/en.json'),
'es': _ => import('./i18n.min/es.json'),
'fr': _ => import('./i18n.min/fr.json'),
'ko': _ => import('./i18n.min/ko.json'),
'ro': _ => import('./i18n.min/ro.json'),
'ru': _ => import('./i18n.min/ru.json'),
'zh': _ => import('./i18n.min/zh.json'),
'zh-TW': _ => import('./i18n.min/zh-TW.json')
};
// Detect human language to use in the UI:
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
const { params } = HashNavigation.parseUrlHash(window.location.hash);
const language = (params && params.hl) ||
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
navigator.userLanguage ||
'en';
// Make sure names like 'en_GB' and 'en-GB' consistently use '-'.
const normalized = language.replace('_', '-');
// Get the base language 'en' from a more specific 'en-US' as a partial fallback.
const baseLanguage = normalized.split('-')[0].toLowerCase();
// Try the full locale first, then the locale without the region code, fallback to 'en'.
const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
// Set lang attribute of the HTML element: <html lang="XX">
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
// Render the app.
const root = createRoot(document.getElementById('mountPoint'));
messageLoader[htmlLang]().then(messages =>
root.render(
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
<TinodeWeb />
</IntlProvider>
));
+307
View File
@@ -0,0 +1,307 @@
// File and image helper functions.
// Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
// Get an URL from a theCard photo: either create a data: URL or return reference URL.
export function makeImageUrl(photo) {
if (photo && typeof photo == 'object') {
if (photo.ref) {
return photo.ref
}
if (photo.data && photo.type) {
const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type);
return 'data:' + mime + ';base64,' + photo.data;
}
}
return null;
}
// Calculate linear dimensions for scaling image down to fit under a certain size.
// Returns an object which contains destination sizes, source sizes, and offsets
// into source (when making square images).
export function fitImageSize(width, height, maxWidth, maxHeight, forceSquare) {
// Sanitize input
width = width | 0;
height = height | 0;
maxWidth = maxWidth | 0;
maxHeight = maxHeight | 0;
if (width <= 0 || height <= 0 || maxWidth <= 0 || maxHeight <= 0) {
return null;
}
if (forceSquare) {
maxWidth = maxHeight = Math.min(maxWidth, maxHeight);
}
const scale = Math.min(
Math.min(width, maxWidth) / width,
Math.min(height, maxHeight) / height
);
const size = {
dstWidth: (width * scale) | 0,
dstHeight: (height * scale) | 0,
};
if (forceSquare) {
// Also calculate parameters for making the image square.
size.dstWidth = size.dstHeight = Math.min(size.dstWidth, size.dstHeight);
size.srcWidth = size.srcHeight = Math.min(width, height);
size.xoffset = ((width - size.srcWidth) / 2) | 0;
size.yoffset = ((height - size.srcWidth) / 2) | 0;
} else {
size.xoffset = size.yoffset = 0;
size.srcWidth = width;
size.srcHeight = height;
}
return size;
}
// Ensure file's extension matches mime content type
export function fileNameForMime(fname, mime) {
const idx = SUPPORTED_IMAGE_FORMATS.indexOf(mime);
if (idx < 0 || !fname) {
// Unknown mime or empty name.
return fname;
}
const ext = MIME_EXTENSIONS[idx];
const at = fname.lastIndexOf('.');
if (at >= 0) {
fname = fname.substring(0, at);
}
return fname + '.' + ext;
}
// Scale uploaded image to fit under certain dimensions and byte size, optionally constraining to a square.
// Returns a promise which is resolven on success or rejected on failure.
export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onerror = function(err) {
reject(new Error("Image format unrecognized"));
}
img.onload = async function() {
// Once the image is loaded, the URL is no longer needed.
URL.revokeObjectURL(img.src);
// Calculate the desired image dimensions.
const dim = fitImageSize(img.width, img.height, maxWidth, maxHeight, forceSquare);
if (!dim) {
reject(new Error("Invalid image"));
return;
}
let canvas = document.createElement('canvas');
canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight;
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.drawImage(img, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
0, 0, dim.dstWidth, dim.dstHeight);
const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
// Generate blob to check size of the image.
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
if (!blob) {
reject(new Error("Unsupported image format"));
return;
}
// Ensure the image is not too large. Shrink the image keeping the aspect ratio.
// Do nothing if maxsize is <= 0.
while (maxSize > 0 && blob.length > maxSize) {
dim.dstWidth = (dim.dstWidth * 0.70710678118) | 0;
dim.dstHeight = (dim.dstHeight * 0.70710678118) | 0;
canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight;
ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
0, 0, dim.dstWidth, dim.dstHeight);
blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
}
canvas = null;
resolve({mime: mime, blob: blob, width: dim.dstWidth, height: dim.dstHeight, name: fileNameForMime(fileOrBlob.name, mime)});
};
img.src = URL.createObjectURL(fileOrBlob);
});
}
// Scale and crop image according to specified dimensions.
// The coordinates are in unscaled image pixels, i.e. cut the rectangle first then scale it.
// returns a promise.
export function imageCrop(mime, objURL, left, top, width, height, scale) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onerror = _ => {
reject(new Error("Image format unrecognized"));
};
img.onload = _ => {
// Once the image is loaded, the URL is no longer needed.
URL.revokeObjectURL(img.src);
let canvas = document.createElement('canvas');
canvas.width = width * scale;
canvas.height = height * scale;
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.drawImage(img, left, top, width, height, 0, 0, canvas.width, canvas.height);
mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg';
// Generate blob to check size of the image.
canvas.toBlob((blob) => {
// Allow GC.
canvas = null;
if (blob) {
resolve({mime: mime, blob: blob, width: width, height: height});
} else {
reject(new Error("Unsupported image format"));
}
}, mime);
};
img.src = objURL;
});
}
// Convert file to base64 string.
export function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = _ => {
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
};
reader.readAsDataURL(file);
});
}
// Convert Blob to base64 string. Returns a promise resolved with the base64 string and mime.
export function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = _ => {
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
};
reader.readAsDataURL(blob);
});
}
// File pasted from the clipboard. It's either an inline image or a file attachment.
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
if (!items || !items.length) {
return false;
}
for (let i in items) {
const item = items[i];
if (item.kind === 'file') {
const file = item.getAsFile();
if (!file) {
console.error("Failed to get file object from pasted file item", item.kind, item.type);
onError("Failed to get file object from pasted file item");
continue;
}
if (file.type && file.type.split('/')[0] == 'image') {
onImageSuccess(file);
} else {
onAttachmentSuccess(file);
}
// Indicate that the pasted data contains a file.
return true;
}
}
// No file found.
return false;
}
// Get mime type from data URL header.
export function getMimeType(header) {
const mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
return (mime && mime.length > 1) ? mime[1] : null;
}
// Given length of a binary object in bytes, calculate the length after
// base64 encoding.
export function base64EncodedLen(n) {
return Math.floor((n + 2) / 3) * 4;
}
// Given length of a base64-encoded object, calculate decoded size of the
// pbject in bytes.
export function base64DecodedLen(n) {
return Math.floor(n / 4) * 3;
}
// Re-encode string to standard base64 encoding with padding.
// The string may be base64-URL encoded without the padding.
export function base64ReEncode(str) {
if (str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
try {
str = btoa(atob(str));
} catch(err) {
console.error("Failed to base64 re-encode string.", err);
str = null;
}
}
return str;
}
// Convert a base64 encoded string with the provided mime type into a Blob.
export function base64ToBlob(str, mime) {
if (!str) {
return null;
}
try {
// Make blob.
const bin = atob(str);
const length = bin.length;
const buf = new ArrayBuffer(length);
const arr = new Uint8Array(buf);
for (let i = 0; i < length; i++) {
arr[i] = bin.charCodeAt(i);
}
return new Blob([buf], { type: mime });
} catch(err) {
console.error("Failed to convert base64 to blob: ", err);
}
return null;
}
export function intArrayToBase64(arr) {
if (!Array.isArray(arr)) {
return null;
}
try {
let bin = '';
new Uint8Array(arr).forEach(b => bin += String.fromCharCode(b));
return window.btoa(bin);
} catch (err) {}
return null;
}
export function base64ToIntArray(b64) {
const arr = [];
try {
const bin = window.atob(b64);
[...bin].forEach(c => {
arr.push(c.charCodeAt(0));
});
return arr;
} catch (err) {}
return null;
}
+543
View File
@@ -0,0 +1,543 @@
import React from 'react';
import { defineMessages } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import AudioPlayer from '../widgets/audio-player.jsx'
import CallMessage from '../widgets/call-message.jsx'
import CallStatus from '../widgets/call-status.jsx';
import InlineVideo from '../widgets/inline-video.jsx';
import LazyImage from '../widgets/lazy-image.jsx'
import UploadingImage from '../widgets/uploading-image.jsx'
import { BROKEN_IMAGE_SIZE, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO, REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
import { cancelablePromise, sanitizeUrlForMime } from './utils.js';
const messages = defineMessages({
drafty_form: {
id: 'drafty_form',
defaultMessage: 'Form: ',
description: 'Comment for form in Drafty'
},
drafty_attachment: {
id: 'drafty_attachment',
defaultMessage: 'Attachment',
description: 'Comment for attachment in Drafty'
},
drafty_image: {
id: 'drafty_image',
defaultMessage: 'Picture',
description: 'Comment for embedded images in Drafty'
},
drafty_video: {
id: 'drafty_video',
defaultMessage: 'Video recording',
description: 'Comment for videos embedded in Drafty'
},
drafty_unknown: {
id: 'drafty_unknown',
defaultMessage: 'Unsupported',
description: 'Unsupported entity in drafty'
}
});
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
// 'this' must contain:
// viewportWidth:
// authorizeURL:
// onImagePreview:
// onVideoPreview:
// onFormButtonClick:
// onQuoteClick:
export function fullFormatter(style, data, values, key, stack) {
if (stack.includes('QQ')) {
return quoteFormatter.call(this, style, data, values, key);
}
if (!style) {
// Unformatted.
return values;
}
let el = Drafty.tagName(style);
let attr = Drafty.attrValue(style, data) || {};
attr.key = key;
switch (style) {
case 'AU':
// Show audio player.
if (attr.src) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'audio'));
attr.duration = data.duration > 0 ? (data.duration | 0) : undefined;
attr.preview = data.preview;
attr.loading = 'lazy';
}
el = AudioPlayer;
// Audio element cannot have content.
values = null;
break;
case 'BR':
values = null;
break;
case 'EX':
// Ignore.
break;
case 'HL':
// Highlighted text. Assign class name.
attr.className = 'highlight';
break;
case 'HD':
el = null;
values = null;
break;
case 'IM':
// Additional processing for images
el = handleImageData.call(this, el, data, attr);
// Image element cannot have content.
values = null;
break;
case 'BN':
// Button
attr.onClick = this.onFormButtonClick;
let inner = React.Children.map(values, (child) => {
return typeof child == 'string' ? child : undefined;
});
if (!inner || inner.length == 0) {
inner = [attr.name]
}
// Get text which will be sent back when the button is clicked.
attr['data-title'] = inner.join('');
break;
case 'MN':
// Mention
attr.className = 'mention'
if (data) {
attr.className += ' ' + idToColorClass(data.val, false, true);
}
break;
case 'FM':
// Form
attr.className = 'bot-form';
break;
case 'RW':
// Form element formatting is dependent on element content.
break;
case 'QQ':
// Quote/citation.
attr.className = 'reply-quote'
attr.onClick = this.onQuoteClick;
break;
case 'VC':
el = CallMessage;
// Video call messages do not have content.
values = null;
if (data) {
attr.callState = data.state;
attr.incoming = data.incoming;
attr.duration = data.duration;
}
break;
case 'VD':
// Additional processing for videos.
el = handleVideoData.call(this, el, data, attr);
// Video element cannot have content.
values = null;
break;
default:
if (!el) {
// Unknown element.
el = React.Fragment;
attr = {key: key};
// Generate comment for unknown element.
let body = values;
if (!Array.isArray(values) || !values.join('').trim()) {
body = [<span key="x1" className="gray">{this.formatMessage(messages.drafty_unknown)}</span>];
}
values = [<i key="x0" className="material-icons gray">extension</i>, ' '].concat(body);
}
break;
}
if (!el) {
return values;
}
return React.createElement(el, attr, values);
}
// Additional processing of image data.
function handleImageData(el, data, attr) {
if (!data) {
attr.src = 'img/broken_image.png';
attr.style = {
width: IMAGE_THUMBNAIL_DIM + 'px',
height: IMAGE_THUMBNAIL_DIM + 'px',
};
return el;
}
attr.className = 'inline-image';
const dim = fitImageSize(data.width, data.height,
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
attr.style = {
width: dim.dstWidth + 'px',
height: dim.dstHeight + 'px',
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
minWidth: dim.dstWidth + 'px',
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
attr.alt = data.name;
if (attr.src) {
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
// Allow previews for large enough images.
attr.onClick = this.onImagePreview;
attr.className += ' image-clickable';
}
attr.loading = 'lazy';
} else {
attr.src = null;
}
} else {
// Use custom element instead of <img>.
el = UploadingImage;
}
return el;
}
// Additional processing of image data.
function handleVideoData(el, data, attr) {
if (!data) {
attr.src = 'img/broken_video.png';
attr.style = {
width: IMAGE_THUMBNAIL_DIM + 'px',
height: IMAGE_THUMBNAIL_DIM + 'px',
};
return el;
}
attr.className = 'inline-image';
const dim = fitImageSize(data.width, data.height,
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
{dstWidth: NO_DIMENSIONS_VIDEO, dstHeight: NO_DIMENSIONS_VIDEO};
attr.style = {
width: dim.dstWidth + 'px',
height: dim.dstHeight + 'px',
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
minWidth: dim.dstWidth + 'px',
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
attr.alt = data.name;
if (data.ref || data.val) {
attr.onClick = this.onVideoPreview;
attr.loading = 'lazy';
}
el = InlineVideo;
} else {
// Use custom element instead of <img>.
el = UploadingImage;
}
return el;
}
// Converts Drafty object into a one-line preview. 'this' is set by the caller.
// 'this' must contain:
// formatMessage: this.props.intl.formatMessage
// messages: formatjs messages defined with defineMessages.
export function previewFormatter(style, data, values, key) {
if (!style) {
// Unformatted.
return values;
}
let el = Drafty.tagName(style);
const attr = { key: key };
switch (style) {
case 'AU':
// Voicemail as '[mic] 0:00'.
el = React.Fragment;
values = [<i key="au" className="material-icons">mic</i>, ' ', secondsToTime(data.duration/1000)];
break;
case 'BR':
// Replace new line with a space.
el = React.Fragment;
values = [' '];
break;
case 'HL':
// Make highlight less prominent in preview.
attr.className = 'highlight preview';
break;
case 'LN':
case 'MN':
// Disable links in previews.
el = 'span';
break;
case 'IM':
// Replace image with '[icon] Image'.
el = React.Fragment;
values = [<i key="im" className="material-icons">photo</i>, ' ', this.formatMessage(messages.drafty_image)];
break;
case 'BN':
el = 'span';
attr.className = 'flat-button faux';
break;
case 'FM':
el = React.Fragment;
values = [<i key="fm" className="material-icons">dashboard</i>,
this.formatMessage(messages.drafty_form)].concat(' ', values || []);
break;
case 'RW':
el = React.Fragment;
break;
case 'EX':
if (data) {
if (data.mime == 'application/json') {
// Ignore JSON attachments: they are form response payloads.
return null;
}
// Clear payload.
delete data.val;
delete data.ref;
}
el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>, ' ', this.formatMessage(messages.drafty_attachment)];
break;
case 'VC':
el = CallStatus;
if (data) {
attr.callState = data.state;
attr.incoming = data.incoming;
attr.duration = data.duration;
}
values = null;
break;
case 'QQ':
case 'HD':
el = null;
values = null;
break;
case 'VD':
// Replace image with '[icon] Video'.
el = React.Fragment;
values = [<i key="im" className="material-icons">play_circle_outline</i>, ' ', this.formatMessage(messages.drafty_video)];
break;
default:
if (!el) {
// Unknown element.
el = React.Fragment;
values = [<i key="x0" className="material-icons gray">extension</i>, ' ', this.formatMessage(messages.drafty_unknown)];
}
break;
}
if (!el) {
return values;
}
return React.createElement(el, attr, values);
};
// Converts Drafty object into a quoted reply; 'this' is set by the caller.
function inlineImageAttr(attr, data) {
attr.style = {
width: IMAGE_THUMBNAIL_DIM + 'px',
height: IMAGE_THUMBNAIL_DIM + 'px',
maxWidth: IMAGE_THUMBNAIL_DIM + 'px',
maxHeight: IMAGE_THUMBNAIL_DIM + 'px',
}
attr.className = 'inline-image';
attr.alt = this.formatMessage(messages.drafty_image);
if (!data) {
attr.src = 'img/broken_image.png';
} else {
attr.src = attr.src || 'img/broken_image.png';
}
attr.title = attr.alt;
return attr;
}
// Converts Drafty object into a quoted reply; 'this' is set by the caller.
function inlineVideoAttr(attr, data) {
const dim = fitImageSize(data.width, data.height, VIDEO_THUMBNAIL_WIDTH, IMAGE_THUMBNAIL_DIM);
attr.style = {
width: dim.width + 'px',
height: dim.height + 'px',
maxWidth: VIDEO_THUMBNAIL_WIDTH + 'px',
maxHeight: IMAGE_THUMBNAIL_DIM + 'px',
}
attr.className = 'inline-image';
attr.alt = this.formatMessage(messages.drafty_video);
attr.title = attr.alt;
if (!data) {
attr.src = 'img/broken_video.png';
} else {
attr.src = attr.src || 'img/broken_video.png';
}
return attr;
}
// Displays a portion of Drafty within 'QQ' quotes. 'this' is set by the caller.
// 'this' must contain:
// formatMessage: this.props.intl.formatMessage
// messages: formatjs messages defined with defineMessages.
// authorizeURL: this.props.tinode.authorizeURL
// onQuoteClick: this.handleQuoteClick (optional)
function quoteFormatter(style, data, values, key) {
if (['BR', 'EX', 'IM', 'MN', 'VD'].includes(style)) {
let el = Drafty.tagName(style);
let attr = Drafty.attrValue(style, data) || {};
attr.key = key;
switch(style) {
case 'BR':
values = null;
break;
case 'IM':
attr = inlineImageAttr.call(this, attr, data);
values = [React.createElement('img', attr, null), ' ', attr.alt];
el = React.Fragment;
// Fragment attributes.
attr = {key: key};
break;
case 'VD':
attr = inlineVideoAttr.call(this, attr, data);
values = [React.createElement('img', attr, null), ' ', attr.alt];
el = React.Fragment;
// Fragment attributes.
attr = {key: key};
break;
case 'MN':
el = 'span';
attr.className = 'mention'
if (data) {
attr.className += ' ' + idToColorClass(data.val, false, true);
}
break;
case 'EX':
let fname;
if (data) {
if (data.mime == 'application/json') {
// Ignore JSON attachments: they are form response payloads.
return null;
}
fname = data.name;
// Clear payload.
delete data.val;
delete data.ref;
}
el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>,
shortenFileName(fname, 16) || this.formatMessage(messages.drafty_attachment)];
break;
}
return React.createElement(el, attr, values);
}
return previewFormatter.call(this, style, data, values, key);
}
// Create image thumbnail suitable for inclusion in a quote.
function quoteImageOrVideo(data, isVideo) {
let promise;
let bits, ref, mime;
if (isVideo) {
bits = data.preview;
mime = data.premime || 'image/jpeg';
ref = data.preref;
} else {
bits = data.val;
mime = data.mime;
ref = data.ref;
}
// Get the blob from the image data.
if (bits) {
const blob = base64ToBlob(bits, mime);
if (!blob) {
throw new Error("Invalid image");
}
promise = Promise.resolve(blob);
} else if (ref) {
promise = fetch(this.authorizeURL(sanitizeUrlForMime(ref, 'image')))
.then(evt => {
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
} else {
throw new Error("Missing image data");
}
// Scale the blob.
return promise
.then(blob => {
// If it's an image, cut the square from the center of the image and shrink it.
// If it's a video, allow it to be rectantular.
return imageScaled(blob, isVideo ? VIDEO_THUMBNAIL_WIDTH : IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, !isVideo)
}).then(scaled => {
if (isVideo) {
data.premime = scaled.mime;
} else {
data.mime = scaled.mime;
}
data.size = scaled.blob.size;
data.width = scaled.width;
data.height = scaled.height;
delete data.ref;
delete data.preref;
// Keeping the original file name, if provided: ex.data.name;
data.src = URL.createObjectURL(scaled.blob);
return blobToBase64(scaled.blob);
}).then(b64 => {
if (isVideo) {
data.preview = b64.bits;
} else {
data.val = b64.bits;
}
return data;
}).catch(err => {
delete data.val;
delete data.preview;
delete data.src;
data.width = IMAGE_THUMBNAIL_DIM;
data.height = IMAGE_THUMBNAIL_DIM;
// Rethrow.
throw err;
});
}
// Create a preview of a reply.
export function replyFormatter(style, data, values, key, stack) {
if (style == 'IM' || style == 'VD') {
const isImage = style == 'IM';
const attr = isImage ? inlineImageAttr.call(this, {key: key}, data) :
inlineVideoAttr.call(this, {key: key}, data);
let loadedPromise;
try {
loadedPromise = cancelablePromise(quoteImageOrVideo.call(this, data, style == 'VD'));
} catch (error) {
console.warn("Failed to quote image:", error.message);
loadedPromise = cancelablePromise(error);
}
attr.whenDone = loadedPromise;
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
return React.createElement(React.Fragment, {key: key}, values);
} else if (style == 'QQ') {
if (stack.includes('QQ')) {
// Quote inside quote when forwarding a message.
return React.createElement('span', {key: key},
[<i key="qq" className="material-icons">format_quote</i>, ' ']);
}
const attr = Drafty.attrValue('QQ', data) || {};
attr.key = key;
attr.className = 'reply-quote'
return React.createElement(Drafty.tagName('QQ'), attr, values);
}
return quoteFormatter.call(this, style, data, values, key);
}
+29
View File
@@ -0,0 +1,29 @@
import { KNOWN_HOSTS, DEFAULT_HOST } from '../config.js';
// Detect server address from the URL
export function detectServerAddress() {
let host = DEFAULT_HOST;
if (typeof window.location == 'object') {
if (window.location.protocol == 'file:' || window.location.hostname == 'localhost') {
host = KNOWN_HOSTS.local;
} else if (window.location.hostname) {
host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
}
}
return host;
}
// Detect if the page is served over HTTPS.
export function isSecureConnection() {
if (typeof window.location == 'object') {
return window.location.protocol == 'https:';
}
return false;
}
export function isLocalHost() {
if (typeof window.location == 'object') {
return window.location.hostname == 'localhost';
}
return false;
}
+26
View File
@@ -0,0 +1,26 @@
// Helper functions for storing values in localStorage.
// By default localStorage can store only strings, not objects or other types.
export default class LocalStorageUtil {
// Replace old object with the new one.
static setObject(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
// Get stored object.
static getObject(key) {
const value = localStorage.getItem(key);
return value && JSON.parse(value);
}
// Partially or wholly update stored object.
static updateObject(key, value) {
const oldVal = this.getObject(key);
this.setObject(key, Object.assign(oldVal || {}, value));
}
// Just a wrapper.
static removeItem(key) {
localStorage.removeItem(key);
}
}
+79
View File
@@ -0,0 +1,79 @@
// Utility class for hash navigation.
export default class HashNavigation {
// Parse hash as in http://www.example.com/path#hash as if it were
// path and arguments.
static parseUrlHash(hash) {
// Split path from args, path -> parts[0], args->path[1]
const parts = hash.split('?', 2);
const params = {};
let path = [];
if (parts[0]) {
path = parts[0].replace('#', '').split('/');
}
if (parts[1]) {
parts[1].split('&').forEach((arg) => {
// Can't use split() because the value may contain '='.
const eq = arg.indexOf('=');
if (eq > 0) {
params[arg.slice(0, eq)] = decodeURIComponent(arg.slice(eq + 1));
}
});
}
return {path: path, params: params};
}
static navigateTo(url) {
window.location.hash = url;
}
static composeUrlHash(path, params) {
let url = path.join('/');
const args = [];
for (const key in params) {
if (params.hasOwnProperty(key)) {
args.push(key + '=' + encodeURIComponent(params[key]));
}
}
if (args.length > 0) {
url += '?' + args.join('&');
}
return url;
}
static addUrlParam(hash, key, value) {
const parsed = HashNavigation.parseUrlHash(hash);
parsed.params[key] = value;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static removeUrlParam(hash, key) {
const parsed = HashNavigation.parseUrlHash(hash);
delete parsed.params[key];
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlSidePanel(hash, sidepanel) {
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[0] = sidepanel;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlInfoPanel(hash, infopanel) {
const parsed = HashNavigation.parseUrlHash(hash);
if (infopanel) {
parsed.params.info = infopanel;
} else {
delete parsed.params.info;
}
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlTopic(hash, topic) {
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[1] = topic;
// Close InfoView on topic change.
delete parsed.params.info;
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
}
+110
View File
@@ -0,0 +1,110 @@
// Short representation of time in the past.
export function shortDateFormat(then, locale) {
locale = locale || window.navigator.userLanguage || window.navigator.language;
const now = new Date();
if (then.getFullYear() == now.getFullYear()) {
// Same year.
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
// Same month and day, show time only.
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
}
// Different month and/or day, show month day, time.
return then.toLocaleDateString(locale,
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
}
// Different year: just show the date.
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
}
// Representation a date relative to now.
export function relativeDateFormat(then, locale) {
locale = locale || window.navigator.userLanguage || window.navigator.language;
const now = new Date();
const thenDays = Math.floor((then.getTime() - then.getTimezoneOffset() * 60_000) / 86_400_000);
const nowDays = Math.floor((now.getTime() - now.getTimezoneOffset() * 60_000) / 86_400_000);
const diff = thenDays - nowDays;
if (Math.abs(diff) < 2) {
// Today or yesterday.
return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(diff, 'day')
}
// More than two days: just show the date.
return new Intl.DateTimeFormat(locale).format(then);
}
// Convert seconds to [hours:]minutes:seconds, i.e. 156 sec -> 2:36, 3756 sec -> 1:02:36.
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
export function secondsToTime(seconds, fixedMin) {
if (typeof seconds != 'number') {
return '';
}
let min = (Math.floor(seconds / 60) | 0) % 60;
let hours = Math.floor(seconds / 3600) | 0;
if (fixedMin || hours > 0) {
min = min < 10 ? `0${min}` : min;
}
let sec = (seconds | 0) % 60;
sec = sec < 10 ? `0${sec}` : sec;
if (hours == 0) {
return `${min}:${sec}`;
}
return `${hours}:${min}:${sec}`;
}
// Convert a number of bytes to human-readable format.
export function bytesToHumanSize(bytes) {
if (!bytes || bytes == 0) {
return '0 Bytes';
}
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const bucket = Math.min(Math.floor(Math.log2(bytes) / 10) | 0, sizes.length-1);
const count = bytes / Math.pow(1024, bucket);
const round = bucket > 0 ? (count < 3 ? 2 : (count < 30 ? 1 : 0)) : 0;
return count.toFixed(round) + ' ' + sizes[bucket];
}
// Shorten a file name to be under maxLength by clipping out the middle.
export function shortenFileName(filename, maxLength) {
if (typeof filename != 'string') {
return filename;
}
return filename.length > maxLength ?
filename.slice(0, maxLength/2 - 1) + '…' + filename.slice(1 - maxLength/2) : filename;
}
// Get 32 bit integer hash value for a string. Ideally it should produce the same value
// as Java's String#hash().
function stringToColorHash(value) {
let hash = 0;
value = '' + value;
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) - hash) + value.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
// User avatar letter color id.
export function letterTileColorId(userId) {
return Math.abs(stringToColorHash(userId)) % 16;
}
// Converts user or topic ID to a CSS color class. Ideally it should produce the same color value as Java version.
// params:
// - {string} id: user or topic ID
// - {boolean} light: light or dark version of the color.
// - {boolean} fg: foreground (text) or background color.
export function idToColorClass(id, light, fg) {
return (light ? 'lt-' : 'dk-') + (fg ? 'fg-' : 'bg-') + letterTileColorId(id);
}
// Takes ISO Alpha-2 country code, like 'US' and converts it to a flag emoji.
export function flagEmoji(countryCode) {
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 0x1F1A5 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}
+213
View File
@@ -0,0 +1,213 @@
// Odds and ends
import { Tinode } from 'tinode-sdk';
// Make shortcut icon appear with a green dot + show unread count in title.
export function updateFavicon(count) {
const oldIcon = document.getElementById('shortcut-icon');
const head = document.head || document.getElementsByTagName('head')[0];
const newIcon = document.createElement('link');
newIcon.type = 'image/png';
newIcon.id = 'shortcut-icon';
newIcon.rel = 'shortcut icon';
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
if (oldIcon) {
head.removeChild(oldIcon);
}
head.appendChild(newIcon);
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
}
// Create theCard which represents user's or topic's "public" info.
export function theCard(fn, imageUrl, imageMimeType, note) {
let card = null;
fn = fn && fn.trim();
note = note && note.trim();
if (fn) {
card = {
fn: fn
};
}
if (typeof note == 'string') {
card = card || {};
card.note = note ? note : Tinode.DEL_CHAR;
}
if (imageUrl) {
card = card || {};
let mimeType = imageMimeType;
// Is this a data URL "data:[<mediatype>][;base64],<data>"?
const matches = /^data:(image\/[-a-z0-9+.]+)?(;base64)?,/i.exec(imageUrl);
if (matches) {
mimeType = matches[1];
card.photo = {
data: imageUrl.substring(imageUrl.indexOf(',') + 1),
ref: Tinode.DEL_CHAR
};
} else {
card.photo = {
data: Tinode.DEL_CHAR,
ref: imageUrl
};
}
card.photo.type = (mimeType || 'image/jpeg').substring('image/'.length);
}
return card;
}
// Deep-shallow compare two arrays: shallow compare each element.
export function arrayEqual(a, b) {
if (a === b) {
return true;
}
if (!Array.isArray(a) || !Array.isArray(b)) {
return false;
}
// Compare lengths first.
if (a.length != b.length) {
return false;
}
// Order of elements is ignored.
a.sort();
b.sort();
for (let i = 0, l = a.length; i < l; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
// Checks (loosely) if the given string is a phone. If so, returns the phone number in a format
// as close to E.164 as possible.
export function asPhone(val) {
val = val.trim();
if (/^(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?$/.test(val)) {
return val.replace(/[- ().]*/, '');
}
return null;
}
// Checks (loosely) if the given string is an email. If so returns the email.
export function asEmail(val) {
val = val.trim();
if (/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(val)) {
return val;
}
return null;
}
// Checks if URL is a relative url, i.e. has no 'scheme://', including the case of missing scheme '//'.
// The scheme is expected to be RFC-compliant, e.g. [a-z][a-z0-9+.-]*
// example.html - ok
// https:example.com - not ok.
// http:/example.com - not ok.
// ' ↲ https://example.com' - not ok. (↲ means carriage return)
export function isUrlRelative(url) {
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
}
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
export function sanitizeUrl(url, allowedSchemes) {
if (typeof url != 'string') {
return url;
}
// Strip control characters and whitespace. They are not valid URL characters anyway.
url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
// Relative URLs are safe.
// Relative URL does not start with ':', abcd123: or '//'.
if (!/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) {
return url;
}
// Blob URLs are safe.
if (/^blob:http/.test(url)) {
return url;
}
// Absolute URL. Accept only safe schemes, or no scheme.
const schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
const re = new RegExp('^((' + schemes + '):|//)', 'i');
if (!re.test(url)) {
return null;
}
return url;
}
// Ensure URL is suitable as a source like <img src="url"> field: the URL must be a relative URL or
// have http:, https:, blob: or data: scheme.
// In case of data: scheme, the URL must must be of the right MIME type such as 'data:{mimeMajor}/XXXX;base64,'.
export function sanitizeUrlForMime(url, mimeMajor) {
if (!url) {
return null;
}
const sanitizedUrl = sanitizeUrl(url);
if (sanitizedUrl) {
return sanitizedUrl;
}
// Is this a data: URL of the appropriate mime type?
const re = new RegExp(`data:${mimeMajor}\/[a-z0-9.-]+;base64,`, 'i');
if (re.test(url.trim())) {
return url;
}
return null;
}
// Given message's received status, return name and color of a delivery indicator icon.
export function deliveryMarker(received) {
switch (received) {
case Tinode.MESSAGE_STATUS_SENDING:
return { name: 'access_time' }; // watch face
case Tinode.MESSAGE_STATUS_FAILED:
case Tinode.MESSAGE_STATUS_FATAL:
return { name: 'warning', color: 'danger-color' }; // yellow icon /!\
case Tinode.MESSAGE_STATUS_SENT:
return { name: 'done' }; // checkmark
case Tinode.MESSAGE_STATUS_RECEIVED:
return { name: 'done_all' }; // double checkmark
case Tinode.MESSAGE_STATUS_READ:
return { name: 'done_all', color: 'blue' }; // blue double checkmark
}
return null;
}
// Wraps a promise to make it cancelable.
// The parameter can be either a promise or an error. If it's an error, the wrapped promise is
// created in a rejected state.
export function cancelablePromise(promise) {
let hasCanceled = false;
const wrappedPromise = promise instanceof Error ?
Promise.reject(promise) :
new Promise((resolve, reject) => {
promise.then(
result => hasCanceled ? reject({isCanceled: true}) : resolve(result),
error => hasCanceled ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled = true;
},
};
};
// Clips string to the specified length.
export function clipStr(str, length) {
return str && str.substring(0, length);
}
+3
View File
@@ -0,0 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.23.0-rc1";
+61
View File
@@ -0,0 +1,61 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
export default class EditAccountView extends React.PureComponent {
constructor(props) {
super(props);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
}
handleCheckboxClick(what, checked) {
if (what == 'sound') {
this.props.onToggleMessageSounds(checked);
} else if (what == 'alert') {
this.props.onTogglePushNotifications(checked);
} else if (what == 'incognito') {
this.props.onToggleIncognitoMode(checked);
}
}
render() {
return (
<div className="scrollable-panel">
<div className="panel-form-row">
<label htmlFor="message-sound">
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
description="Label for message sounds toggle" />
</label>
<CheckBox name="sound" id="message-sound"
checked={this.props.messageSounds} onChange={this.handleCheckboxClick} />
</div>
<div className="panel-form-row">
<label htmlFor="desktop-alerts">
{this.props.desktopAlertsEnabled ?
<FormattedMessage id="label_push_notifications" defaultMessage="Notification alerts:"
description="Label for push notifications switch" />
:
<FormattedMessage id="label_push_notifications_disabled"
defaultMessage="Notification alerts (requires HTTPS):"
description="Label for push notifications switch" />
}
</label>
<CheckBox name="alert" id="desktop-alerts"
checked={this.props.desktopAlerts}
onChange={this.props.desktopAlertsEnabled ? this.handleCheckboxClick : null} />
</div>
<div className="panel-form-row">
<label htmlFor="incognito-mode">
<FormattedMessage id="label_incognito_mode" defaultMessage="Incognito mode:"
description="Label for incognito mode toggle" />
</label>
<CheckBox name="incognito" id="incognito-mode"
checked={this.props.incognitoMode} onChange={this.handleCheckboxClick} />
</div>
</div>
);
}
};
+155
View File
@@ -0,0 +1,155 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
const messages = defineMessages({
delete_account: {
id: 'delete_account',
defaultMessage: 'Delete account',
description: 'Title for delete account warning'
},
delete_account_warning: {
id: 'delete_account_warning',
defaultMessage: 'Are you sure you want to delete your account? It cannot be undone.',
description: 'Warning message when deleting an account'
}
});
class AccSecurityView extends React.Component {
constructor(props) {
super(props);
const me = this.props.tinode.getMeTopic();
let blockedCount = 0;
me.contacts((c) => {
if (c.acs && !c.acs.isJoiner()) {
blockedCount ++;
}
});
const defacs = me.getDefaultAccess();
this.state = {
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
showPermissionEditorFor: undefined,
blockedCount: blockedCount,
};
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
}
handlePasswordUpdate(pwd) {
this.setState({password: pwd});
this.props.onUpdatePassword(pwd);
}
handleLaunchPermissionsEditor(which) {
this.setState({
showPermissionEditorFor: which,
editedPermissions: this.state[which]
});
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
}
handlePermissionsChanged(perm) {
let defacs = {};
defacs[this.state.showPermissionEditorFor] = perm;
this.props.onUpdateAccountDesc('me', undefined, undefined, defacs);
let newState = {showPermissionEditorFor: undefined};
newState[this.state.showPermissionEditorFor] = perm;
this.setState(newState);
}
handleDeleteAccount(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.delete_account), // title
formatMessage(messages.delete_account_warning), // content
(_ => this.props.onDeleteAccount()), // onConfirm
null, // use default text "OK"
true, // Show Reject button
null // use default text "Cancel"
);
}
render() {
return (
<>{this.state.showPermissionEditorFor ?
<PermissionsEditor
mode={this.state.editedPermissions}
skip="O"
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor} />
:
<div className="scrollable-panel">
<div className="panel-form-column">
<label className="small">
<FormattedMessage id="label_password" defaultMessage="Password"
description="Label for password editing" />
</label>
<div>
<FormattedMessage id="password_unchanged_prompt" defaultMessage="Unchanged"
description="Message in editor while password is unchanged">{
(password_unchanged) => <InPlaceEdit
placeholder={password_unchanged}
type="password"
onFinished={this.handlePasswordUpdate} />
}</FormattedMessage>
</div>
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="danger flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
<i className="material-icons">exit_to_app</i> &nbsp;<FormattedMessage id="button_logout"
defaultMessage="Logout" description="Button [Logout]" />
</a>
<a href="#" className="danger flat-button" onClick={(e) => {this.handleDeleteAccount(e);}}>
<i className="material-icons">delete</i> &nbsp;<FormattedMessage id="button_delete_account"
defaultMessage="Delete account" description="Button [Delete account]" />
</a>
</div>
<div className="hr" />
<div className="panel-form-column">
<div>
<label className="small">
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
description="Label for default access mode" />
</label>
</div>
<div className="quoted">
<div>Auth: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
<div>Anon: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
</div>
</div>
{this.state.blockedCount > 0 ?
<>
<div className="hr" />
<div className="panel-form-row">
<i className="material-icons">block</i>&nbsp;
<a href="#" className="gray" onClick={(e) => {e.preventDefault(); this.props.onShowBlocked();}}>
<FormattedMessage id="blocked_contacts_link" defaultMessage="Blocked contacts ({count})"
values={{ count: this.state.blockedCount }} description="Blocked contacts link" />
</a>
</div>
</>
: null }
</div>
}</>
);
}
};
export default injectIntl(AccSecurityView);
+51
View File
@@ -0,0 +1,51 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Tinode } from 'tinode-sdk';
import { APP_NAME, LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
export default class AccSupportView extends React.PureComponent {
render() {
return (
<div className="scrollable-panel">
<div className="panel-form-column">
<a href={LINK_CONTACT_US} className="flat-button" target="_blank">
<i className="material-icons">email</i> &nbsp;<FormattedMessage id="link_contact_us"
defaultMessage="Contact Us" description="Ancor text for contacting us by email" />
</a>
<a href={LINK_TERMS_OF_SERVICE} className="flat-button" target="_blank">
<i className="material-icons">description</i> &nbsp;<FormattedMessage id="link_terms_of_service"
defaultMessage="Terms of Service" description="Ancor text for terms of service link" />
</a>
<a href={LINK_PRIVACY_POLICY} className="flat-button" target="_blank">
<i className="material-icons">policy</i> &nbsp;<FormattedMessage id="link_privacy_policy"
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
</a>
</div>
<div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_client" defaultMessage="Client:"
description="Label for a client version" /></label>
{APP_NAME}
</div>
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_sdk" defaultMessage="SDK:" /></label>
{Tinode.getLibrary()}
</div>
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_server" defaultMessage="Server:"
description="Label for a server version" /></label>
{this.props.serverVersion}
</div>
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_server_address" defaultMessage="Server address:" /></label>
{this.props.serverAddress}
</div>
</div>
</div>
);
}
};
+152
View File
@@ -0,0 +1,152 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { parsePhoneNumber } from 'libphonenumber-js/mobile';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
import CredentialEdit from '../widgets/credential-edit.jsx';
function _clip(str, length) {
return str && str.substring(0, length);
}
export default class AccountSettingsView extends React.Component {
constructor(props) {
super(props);
const me = this.props.tinode.getMeTopic();
this.state = {
fullName: _clip(me.public ? me.public.fn : undefined, MAX_TITLE_LENGTH),
description: _clip(me.public ? me.public.note : undefined, MAX_TOPIC_DESCRIPTION_LENGTH),
avatar: makeImageUrl(me.public ? me.public.photo : null),
credentials: me.getCredentials() || [],
credEdit: undefined
};
}
componentDidMount() {
const me = this.props.tinode.getMeTopic();
me.onCredsUpdated = _ => this.setState({credentials: me.getCredentials()});
}
componentWillUnmount() {
const me = this.props.tinode.getMeTopic();
me.onCredsUpdated = null;
}
render() {
if (this.state.credEdit) {
return (
<CredentialEdit
method={this.state.credEdit.meth}
val={this.state.credEdit.val}
done={this.state.credEdit.done}
onShowCountrySelector={this.props.onShowCountrySelector}
onSubmit={this.props.onCredAdd}
onCancel={_ => this.setState({credEdit: undefined})}
onError={this.props.onError} />
);
}
const credentials = [];
this.state.credentials.forEach((cred, idx) => {
if (!['email', 'tel'].includes(cred.meth)) {
// Skip unknown methods.
return ;
}
let val = cred.val
if (cred.meth == 'tel') {
const number = parsePhoneNumber(cred.val);
val = number ? number.formatInternational() : cred.val;
}
credentials.push(
<div className="group quoted" key={idx}>
<tt className="clickable" onClick={e => {e.preventDefault(); this.setState({credEdit: cred});}}>{val}</tt>
<span> {
cred.done ? null :
<>
<i className="material-icons">pending</i>&nbsp;
<a href="#" onClick={e => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}>
<i className="material-icons">delete</i>
</a>
</>
}</span>
</div>);
});
if (credentials.length > 0) {
// Add title as the first element.
credentials.unshift(
<label className="small" key={'title'}>
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts"
description="Label for user contacts" />
</label>);
}
return (
<div className="scrollable-panel">
<div className="panel-form-column">
<a href="#" className="flat-button float-right" onClick={e => {e.preventDefault(); this.props.onNavigate('general');}}>
<i className="material-icons">edit</i>&nbsp;
<FormattedMessage id="button_edit" defaultMessage="Edit" description="Call to action [Edit]" />
</a>
<center>
<AvatarUpload
tinode={this.props.tinode}
avatar={this.state.avatar}
readOnly={true}
uid={this.props.myUserId}
title={this.state.fullName} />
</center>
<div className="group">
<label className="small">
<FormattedMessage id="label_your_name" defaultMessage="Your name"
description="Label for full name editing" />
</label>
<div className="large ellipsized">{this.state.fullName}</div>
</div>
<div className="group">
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
description="Label for user address (ID)" /></label>&nbsp;
<tt>{this.props.myUserId}</tt>
</div>
<div className="group">
<BadgeList trustedBadges={this.props.trustedBadges} />
</div>
{this.state.description ?
<div className="group">
<label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
</label>
<div className="quoted">{this.state.description}</div>
</div> : null}
</div>
<div className="hr" />
<div className="panel-form-column">
<div className="group">{credentials}</div>
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="flat-button" onClick={e => {e.preventDefault(); this.props.onNavigate('notif');}}>
<i className="material-icons">notifications</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_notifications"
defaultMessage="Notifications" description="Sidepanel title for AccNotificationsView." />
</a>
<a href="#" className="flat-button" onClick={e => {e.preventDefault(); this.props.onNavigate('security');}}>
<i className="material-icons">security</i>&nbsp;<FormattedMessage id="button_security"
defaultMessage="Security" description="Navigaton button for security panel." />
</a>
<a href="#" className="flat-button" onClick={e => {e.preventDefault(); this.props.onNavigate('support');}}>
<i className="material-icons">contact_support</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_support"
defaultMessage="Support" description="Sidepanel title for AccSupportView." />
</a>
</div>
</div>
);
}
};
+106
View File
@@ -0,0 +1,106 @@
/* ContactsView holds all contacts-related stuff */
import React from 'react';
import { FormattedMessage, defineMessages } from 'react-intl';
import ContactList from '../widgets/contact-list.jsx';
import { updateFavicon } from '../lib/utils.js';
const messages = defineMessages({
archived_contacts_title: {
id: "archived_contacts",
defaultMessage: "Archived contacts ({count})",
description: "Label for archived chats"
}
});
export default class ContactsView extends React.Component {
constructor(props) {
super(props);
this.handleAction = this.handleAction.bind(this);
this.state = ContactsView.deriveStateFromProps(props);
}
static deriveStateFromProps(props) {
const contacts = [];
let unreadThreads = 0;
let archivedCount = 0;
props.chatList.forEach(c => {
const blocked = c.acs && !c.acs.isJoiner();
// Show only blocked contacts only when props.blocked == true.
if (blocked && props.blocked) {
contacts.push(c);
}
if (blocked || props.blocked) {
return;
}
if (c.private && c.private.arch) {
if (props.archive) {
contacts.push(c);
} else {
archivedCount ++;
}
} else if (!props.archive) {
contacts.push(c);
unreadThreads += c.unread > 0 ? 1 : 0;
}
});
contacts.sort((a, b) => {
return (b.touched || 0) - (a.touched || 0);
});
if (archivedCount > 0) {
contacts.push({
action: 'archive',
title: messages.archived_contacts_title,
values: {count: archivedCount}
});
}
return {
contactList: contacts,
unreadThreads: unreadThreads
};
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.chatList != this.props.chatList ||
prevProps.archive != this.props.archive ||
prevProps.blocked != this.props.blocked) {
const newState = ContactsView.deriveStateFromProps(this.props);
this.setState(newState);
if (newState.unreadThreads != prevState.unreadThreads) {
updateFavicon(newState.unreadThreads);
}
}
}
handleAction(action_ignored) {
this.props.onShowArchive();
}
render() {
return (
<FormattedMessage id="contacts_not_found"
defaultMessage="You have no chats<br />¯∖_(ツ)_/¯"
description="HTML message shown in ContactList when no contacts are found">{
(no_contacts) => <ContactList
tinode={this.props.tinode}
connected={this.props.connected}
contacts={this.state.contactList}
emptyListMessage={no_contacts}
topicSelected={this.props.topicSelected}
myUserId={this.props.myUserId}
showOnline={true}
showUnread={true}
onTopicSelected={this.props.onTopicSelected}
showContextMenu={this.props.showContextMenu}
onAction={this.handleAction} />
}</FormattedMessage>
);
}
};
+228
View File
@@ -0,0 +1,228 @@
// Account registration.
import React, { Suspense } from 'react';
import { FormattedMessage } from 'react-intl';
import AvatarCrop from '../widgets/avatar-crop.jsx';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import CheckBox from '../widgets/checkbox.jsx';
import PhoneEdit from '../widgets/phone-edit.jsx';
import VisiblePassword from '../widgets/visible-password.jsx';
import LocalStorageUtil from '../lib/local-storage.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { theCard } from '../lib/utils.js';
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
MAX_TITLE_LENGTH } from '../config.js';
export default class CreateAccountView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
login: '',
password: '',
meth: '',
email: '',
tel: '',
fn: '', // full/formatted name
imageUrl: null,
uploadUrl: null,
newAvatar: null,
newAvatarMime: null,
buttonDisabled: false,
saveToken: LocalStorageUtil.getObject('keep-logged-in')
};
this.handleLoginChange = this.handleLoginChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handlePhoneChange = this.handlePhoneChange.bind(this);
this.handleFnChange = this.handleFnChange.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleToggleSaveToken = this.handleToggleSaveToken.bind(this);
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
this.uploadAvatar = this.uploadAvatar.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
// Connection will trigger change by changing the this.props.serverVersion.
props.tinode.connect()
.catch(err => {
this.props.onError(err.message, 'err');
});
}
handleLoginChange(e) {
this.setState({login: e.target.value});
}
handlePasswordChange(password) {
this.setState({password: password});
}
handleEmailChange(e) {
this.setState({meth: 'email', email: e.target.value});
}
handlePhoneChange(number) {
this.setState({meth: 'tel', tel: number});
}
handleFnChange(e) {
this.setState({fn: e.target.value});
}
handleImageChanged(mime, img) {
this.setState({newAvatar: img, newAvatarMime: mime});
}
handleToggleSaveToken() {
LocalStorageUtil.setObject('keep-logged-in', !this.state.saveToken);
this.setState({saveToken: !this.state.saveToken});
}
handleSubmit(e) {
e.preventDefault();
this.props.onCreateAccount(
this.state.login.trim(),
this.state.password.trim(),
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.uploadUrl),
{
'meth': this.state.meth,
'val': this.state.meth == 'email' ?
this.state.email :
this.state.meth == 'tel' ? this.state.tel : null
}
);
}
// AvatarCropView calls this method when the user has cropped the image.
handleAvatarCropped(mime, blob, width, height) {
const url = blob ? URL.createObjectURL(blob) : null;
this.setState({avatar: url, newAvatar: null, newAvatarMime: null});
if (blob) {
this.uploadAvatar(mime, blob, width, height);
}
}
handleAvatarCropCancel() {
this.setState({newAvatar: null, newAvatarMime: null});
}
// Utility method for converting cropped avatar blob to bytes for sending inband or
// for uploading it to the server out of band.
uploadAvatar(mime, blob, width, height) {
const readyToUpload = image => {
let {mime, blob} = image;
this.setState({imageUrl: URL.createObjectURL(blob), buttonDisabled: true});
if (blob.size > MAX_AVATAR_BYTES) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
uploader.upload(blob, 'newacc')
.then(url => this.setState({uploadUrl: url}))
.catch(err => this.props.onError(err.message, 'err'))
.finally(_ => this.setState({buttonDisabled: false}));
} else {
// Convert blob to base64-encoded bits.
blobToBase64(blob)
.then(b64 => this.setState({uploadUrl: makeImageUrl({data: b64.bits, type: mime})}))
.finally(_ => this.setState({buttonDisabled: false}));
}
};
if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) {
// Avatar is not square or too large even after cropping. Shrink it and make square.
imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true)
.then(scaled => readyToUpload(scaled))
.catch(err => this.props.onError(err.message, 'err'));
} else {
readyToUpload({mime: mime, blob: blob, width: width, height: height});
}
}
render() {
if (this.state.newAvatar) {
return (
<AvatarCrop
avatar={this.state.newAvatar}
mime={this.state.newAvatarMime}
onSubmit={this.handleAvatarCropped}
onCancel={this.handleAvatarCropCancel}
onError={this.props.onError} />
);
}
let submitClasses = 'primary';
if (this.props.disabled) {
submitClasses += ' disabled';
}
return (
<form className="panel-form-column" onSubmit={this.handleSubmit}>
<div className="panel-form-row">
<div className="umn">
<FormattedMessage id="login_prompt" defaultMessage="Login"
description="Placeholer for username/login">{
(login_prompt) => <input type="text" placeholder={login_prompt} autoComplete="user-name"
value={this.state.login} onChange={this.handleLoginChange} required autoFocus />
}</FormattedMessage>
<FormattedMessage id="password_prompt" defaultMessage="Password"
description="Placeholder/prompt for entering password">{
(password_prompt) => <VisiblePassword placeholder={password_prompt} autoComplete="new-password"
value={this.state.password} onFinished={this.handlePasswordChange}
required={true} />
}</FormattedMessage>
</div>
<AvatarUpload
tinode={this.props.tinode}
avatar={this.state.imageUrl}
onImageUpdated={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="panel-form-row">
<FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe"
description="Input placeholder for person's full name">{
(full_name_prompt) => <input type="text" placeholder={full_name_prompt} autoComplete="name"
value={this.state.fn} onChange={this.handleFnChange} required/>
}</FormattedMessage>
</div>
{this.props.reqCredMethod == 'email' ?
<div className="panel-form-row">
<FormattedMessage id="email_prompt" defaultMessage="Email, e.g. jdoe@example.com"
description="Input placeholder for email entry">{
(email_prompt) => <input type="email" placeholder={email_prompt} autoComplete="email"
value={this.state.email} onChange={this.handleEmailChange} required/>
}</FormattedMessage>
</div>
: this.props.reqCredMethod == 'tel' ?
<><div className="panel-form-row">
<label className="small gray"><FormattedMessage id="mobile_phone_number" defaultMessage="Mobile phone number"
description="Prompt for entering a mobile phone number" /></label>
</div>
<div className="panel-form-row">
<PhoneEdit
autoFocus={false}
onShowCountrySelector={this.props.onShowCountrySelector}
onSubmit={this.handlePhoneChange} />
</div></>
: null
}
<div className="panel-form-row">
<CheckBox id="save-token" name="save-token" checked={this.state.saveToken}
onChange={this.handleToggleSaveToken} />
<FormattedMessage id="stay_logged_in" defaultMessage="Stay logged in"
description="Label for a checkbox">{
(stay_logged_in) => <label htmlFor="save-token">&nbsp;{stay_logged_in}</label>
}</FormattedMessage>
</div>
<div className="dialog-buttons">
<button className={submitClasses} type="submit" disabled={this.state.buttonDisabled}>
<FormattedMessage id="button_sign_up" defaultMessage="Sign up"
description="Create account button [Sign Up]" />
</button>
</div>
</form>
);
}
};
+634
View File
@@ -0,0 +1,634 @@
// InfoView: panel with topic/user info.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { AccessMode, Tinode } from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx';
import CheckBox from '../widgets/checkbox.jsx';
import ContactList from '../widgets/contact-list.jsx';
import ErrorPanel from '../widgets/error-panel.jsx';
import GroupManager from '../widgets/group-manager.jsx';
import MenuCancel from '../widgets/menu-cancel.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TopicCommonView from './topic-common-view.jsx';
import TopicSecurity from '../widgets/topic-security.jsx';
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH, NO_ACCESS_MODE } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { theCard, clipStr } from '../lib/utils.js';
const messages = defineMessages({
info: {
id: 'panel_title_info',
description: 'Title for InfoView',
defaultMessage: 'Info'
},
general: {
id: 'panel_title_general',
description: 'Title for TopicCommon.',
defaultMessage: 'General'
},
security: {
id: 'panel_title_security',
description: 'Title for TopicSecirity and AccSecurity.',
defaultMessage: 'Security'
},
members: {
id: 'panel_title_members',
description: 'Title for managing group members view.',
defaultMessage: 'Members'
},
crop: {
id: 'panel_title_crop',
description: 'Title for AvatarCropView.',
defaultMessage: 'Drag to Adjust'
},
perm_want: {
id: 'requested_permissions',
defaultMessage: 'Requested',
description: 'Title for permissions'
},
perm_given: {
id: 'granted_permissions',
defaultMessage: 'Granted',
description: 'Title for permissions'
},
perm_auth: {
id: 'permissions_authenticated',
defaultMessage: 'Authenticated',
description: 'Title for editing default authenticated permissions'
},
perm_anon: {
id: 'permissions_anonymous',
defaultMessage: 'Anonymous',
description: 'Title for editing default anonymous permissions'
},
perm_user: {
id: 'permissions_user',
defaultMessage: 'User\'s Permissions',
description: 'Title for editing user\'s permissions'
},
edit_permissions: {
id: 'menu_item_edit_permissions',
defaultMessage: 'Edit permissions',
description: 'Menu item [Edit permissions]'
},
});
class InfoView extends React.Component {
constructor(props) {
super(props);
this.state = {
topic: null,
owner: false,
admin: false,
sharer: false,
deleter: false,
muted: false,
address: null,
groupTopic: undefined,
channel: undefined,
fullName: undefined,
description: undefined,
avatar: null,
private: null,
selectedContact: null,
access: null,
modeGiven: null,
modeWant: null,
modeGiven2: null, // P2P topic, the other user mode given
modeWant2: null, // P2P topic, the other user mode want
auth: null,
anon: null,
contactList: [],
trustedBadges: [],
previousMetaDesc: undefined,
previousSubsUpdated: undefined,
};
this.resetSubs = this.resetSubs.bind(this);
this.resetDesc = this.resetDesc.bind(this);
this.resetTags = this.resetTags.bind(this);
this.onMetaDesc = this.onMetaDesc.bind(this);
this.onSubsUpdated = this.onSubsUpdated.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleMuted = this.handleMuted.bind(this);
this.handleUnarchive = this.handleUnarchive.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this);
this.handleContextMenu = this.handleContextMenu.bind(this);
this.handleBackNavigate = this.handleBackNavigate.bind(this);
}
// No need to separately handle component mount.
componentDidUpdate(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
if (this.onMetaDesc != topic.onMetaDesc) {
this.previousMetaDesc = topic.onMetaDesc;
topic.onMetaDesc = this.onMetaDesc;
this.previousSubsUpdated = topic.onSubsUpdated;
topic.onSubsUpdated = this.onSubsUpdated;
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
this.resetDesc(topic, props);
this.resetSubs(topic, props);
this.resetTags(topic);
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.setState({topic: null});
topic.onMetaDesc = this.previousMetaDesc;
topic.onSubsUpdated = this.previousSubsUpdated;
}
resetSubs(topic, props) {
const newState = {contactList: []};
if (topic.getType() == 'p2p') {
// Fetch the other party in the p2p conversation.
// Topic may not be ready yet, so check if user is found.
const user2 = topic.subscriber(props.topic);
if (user2) {
newState.modeGiven2 = user2.acs.getGiven();
newState.modeWant2 = user2.acs.getWant();
} else {
newState.modeGiven2 = NO_ACCESS_MODE;
newState.modeWant2 = NO_ACCESS_MODE;
}
} else {
topic.subscribers((sub) => {
newState.contactList.push(sub);
}, this);
}
this.setState(newState);
}
resetDesc(topic, props) {
const defacs = topic.getDefaultAccess() || {};
const acs = topic.getAccessMode();
const badges = [];
if (topic.trusted) {
for (const [key, val] of Object.entries(topic.trusted)) {
if (val) {
badges.push(key);
}
}
}
this.setState({
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && acs.isSharer(),
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(),
fullName: clipStr(topic.public && topic.public.fn, MAX_TITLE_LENGTH),
description: clipStr(topic.public && topic.public.note, MAX_TOPIC_DESCRIPTION_LENGTH),
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
trustedBadges: badges,
private: clipStr(topic.private && topic.private.comment, MAX_TITLE_LENGTH),
archived: topic.isArchived(),
address: topic.name,
groupTopic: topic.isGroupType(),
channel: topic.isChannelType() || topic.chan,
access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined,
modeWant: acs ? acs.getWant() : undefined,
auth: defacs.auth,
anon: defacs.anon
});
}
resetTags(topic) {
if (topic.getType() != 'grp') {
return;
}
const acs = topic.getAccessMode();
if (acs && acs.isOwner()) {
// Requesting tags: owner is editing the topic.
topic.getMeta(topic.startMetaQuery().withTags().build());
}
}
onMetaDesc(desc) {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetDesc(topic, this.props);
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
this.previousMetaDesc(desc);
}
}
onSubsUpdated(subs) {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetSubs(topic, this.props);
if (this.previousSubsUpdated && this.previousSubsUpdated != this.onSubsUpdated) {
this.previousSubsUpdated(subs);
}
}
handleImageChanged(mime, img) {
this.setState({avatar: img});
this.props.onTopicDescUpdate(this.props.topic, theCard(null, img || Tinode.DEL_CHAR), null);
}
handleMuted(ignored, checked) {
this.setState({muted: checked});
this.props.onChangePermissions(this.props.topic, checked ? '-P' : '+P');
}
handleUnarchive(ignored, ignored2) {
this.props.onTopicUnArchive(this.props.topic);
}
handlePermissionsChanged(which, perm) {
switch (which) {
case 'auth':
this.props.onTopicDescUpdateRequest(this.props.topic, null, null, {auth: perm});
break;
case 'anon':
this.props.onTopicDescUpdateRequest(this.props.topic, null, null, {anon: perm});
break;
case 'mode':
case 'want':
this.props.onChangePermissions(this.props.topic, perm);
break;
case 'given':
this.props.onChangePermissions(this.props.topic, perm, this.props.topic);
break;
case 'user':
this.props.onChangePermissions(this.props.topic, perm, this.state.userPermissionsEdited);
break;
}
this.handleBackNavigate();
}
handleLaunchPermissionsEditor(which, uid) {
const {formatMessage} = this.props.intl;
let toEdit, toCompare, toSkip, titleEdit, titleCompare, userTitle, userAvatar;
switch (which) {
case 'mode':
toEdit = this.state.access;
break;
case 'want':
toEdit = this.state.modeWant;
toCompare = this.state.modeGiven;
if (this.state.owner) {
// Don't allow owner to unset 'O' permission. All other permissions are OK.
toSkip = 'O';
} else {
// Allow accepting any of 'ASDO' permissions but don't allow asking for them.
toSkip = AccessMode.encode(AccessMode.diff('ASDO', this.state.modeGiven));
if (this.state.channel) {
// Channels are read-only.
toSkip += 'W';
}
}
titleEdit = formatMessage(messages.perm_want);
titleCompare = formatMessage(messages.perm_given);
break;
case 'given':
toEdit = this.state.modeGiven2;
toCompare = this.state.modeWant2;
toSkip = this.state.groupTopic ? (this.state.owner ? '' : 'O') : 'ASDO';
titleEdit = formatMessage(messages.perm_given);
titleCompare = formatMessage(messages.perm_want);
break;
case 'auth':
toEdit = this.state.auth;
toSkip = 'O';
break;
case 'anon':
toEdit = this.state.anon;
toSkip = 'O';
break;
case 'user': {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
const user = topic.subscriber(uid);
if (!user || !user.acs) {
return;
}
toEdit = user.acs.getGiven();
toCompare = user.acs.getWant();
toSkip = this.state.owner ? '' : 'O';
titleEdit = formatMessage(messages.perm_given);
titleCompare = formatMessage(messages.perm_want);
if (user.public) {
userTitle = user.public.fn;
userAvatar = user.public.photo;
}
break;
}
default:
console.error("Unknown permission editing mode '" + which + "'");
return;
}
this.setState({
userPermissionsEdited: uid,
userPermissionsTitle: userTitle,
userPermissionsAvatar: userAvatar,
editedPermissions: toEdit,
immutablePermissions: toCompare,
editedPermissionsTitle: titleEdit,
immutablePermissionsTitle: titleCompare,
editedPermissionsSkipped: toSkip,
});
this.props.onNavigate(`perm/${which}`);
}
handleShowAddMembers(e) {
e.preventDefault();
this.props.onInitFind();
this.props.onNavigate('members');
}
handleMemberUpdateRequest(members, added, removed) {
this.props.onMemberUpdateRequest(this.props.topic, added, removed);
this.props.onNavigate('info');
}
handleMemberSelected(uid) {
this.setState({selectedContact: uid});
}
handleBackNavigate() {
const args = (this.props.panel || 'info').split('/');
if (args[0] == 'info') {
this.props.onNavigate(null);
} else if (args[0] == 'perm') {
if (args[1] == 'user') {
this.props.onNavigate('info');
} else {
this.props.onNavigate('security');
}
} else {
this.props.onNavigate('info');
}
}
handleContextMenu(params) {
const {formatMessage} = this.props.intl;
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
const user = topic.subscriber(params.topicName);
if (!user || !user.acs) {
return;
}
const isMe = this.props.tinode.isMe(params.topicName);
const menuItems = [{
title: formatMessage(messages.edit_permissions),
handler: _ => this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName)
}];
if (!isMe) {
menuItems.push('member_delete');
}
menuItems.push(user.acs.isMuted() ? 'member_unmute' : 'member_mute');
if (!isMe) {
menuItems.push(user.acs.isJoiner() ? 'member_block' : 'member_unblock');
}
this.props.showContextMenu({
topicName: this.props.topic,
x: params.x,
y: params.y,
user: params.topicName}, menuItems);
}
render() {
const args = (this.props.panel || 'info').split('/');
const view = args[0];
args.shift();
const {formatMessage} = this.props.intl;
const panelTitle = formatMessage((view == 'perm' ? messages['perm_' + args[0]] : messages[view])
|| messages['info']);
return (
<div id="info-view">
<div className="caption-panel" id="info-caption-panel">
<div className="panel-title" id="info-title">{panelTitle}</div>
<div>
<MenuCancel onCancel={this.handleBackNavigate} />
</div>
</div>
{this.props.displayMobile ?
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
onClearError={this.props.onError} /> : null}
{view == 'members' ?
<GroupManager
tinode={this.props.tinode}
members={this.state.contactList}
requiredMember={this.props.myUserId}
keepInitialMembers={!this.state.admin && !this.state.owner}
myUserId={this.props.myUserId}
contacts={this.props.searchableContacts}
onCancel={this.handleBackNavigate}
onSubmit={this.handleMemberUpdateRequest} />
:
view == 'perm' && args.length > 0 ?
<PermissionsEditor
tinode={this.props.tinode}
mode={this.state.editedPermissions}
compare={this.state.immutablePermissions}
skip={this.state.editedPermissionsSkipped}
modeTitle={this.state.editedPermissionsTitle}
compareTitle={this.state.immutablePermissionsTitle}
userTitle={this.state.userPermissionsTitle}
item={this.state.userPermissionsEdited}
userAvatar={this.state.userPermissionsAvatar}
onSubmit={(mode) => this.handlePermissionsChanged(args[0], mode)}
onCancel={this.handleBackNavigate} />
:
view == 'general' ?
<TopicCommonView
tinode={this.props.tinode}
topic={this.props.topic}
reqCredMethod={this.props.reqCredMethod}
onCredAdd={this.props.onCredAdd}
onUpdateTagsRequest={this.props.onTopicTagsUpdateRequest}
onCredConfirm={this.props.onCredConfirm}
onCredDelete={this.props.onCredDelete}
onUpdateTopicDesc={this.props.onTopicDescUpdateRequest}
onError={this.props.onError} />
:
view == 'security' ?
<TopicSecurity
topic={this.props.topic}
owner={this.state.owner}
admin={this.state.admin}
sharer={this.state.sharer}
deleter={this.state.deleter}
muted={this.state.muted}
groupTopic={this.state.groupTopic}
channel={this.state.channel}
access={this.state.access}
modeGiven={this.state.modeGiven}
modeWant={this.state.modeWant}
modeGiven2={this.state.modeGiven2}
modeWant2={this.state.modeWant2}
auth={this.state.auth}
anon={this.state.anon}
onShowAlert={this.props.onShowAlert}
onDeleteMessages={this.props.onDeleteMessages}
onLeaveTopic={this.props.onLeaveTopic}
onBlockTopic={this.props.onBlockTopic}
onReportTopic={this.props.onReportTopic}
onLaunchPermissionsEditor={this.handleLaunchPermissionsEditor}
onNavigate={this.props.onNavigate} />
:
<div id="info-view-content" className="scrollable-panel">
<div className="panel-form-column">
<a href="#" className="flat-button float-right" onClick={(e) => {e.preventDefault(); this.props.onNavigate('general');}}>
<i className="material-icons">edit</i>&nbsp;
<FormattedMessage id="button_edit" defaultMessage="Edit" description="Call to action [Edit]" />
</a>
<center>
<AvatarUpload
tinode={this.props.tinode}
avatar={this.state.avatar}
readOnly={true}
uid={this.props.topic}
title={this.state.fullName} />
</center>
<div className="group">
<div><label className="small">
<FormattedMessage id="label_topic_name" defaultMessage="Name"
description="Label for editing topic name" />
</label></div>
<div className="large ellipsized">{this.state.fullName}
{this.state.channel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}</div>
</div>
{this.state.private ?
<div className="group">
<div><label className="small">
<FormattedMessage id="label_private" defaultMessage="Private comment"
description="Label for editing 'private'" />
</label></div>
<div className="large ellipsized">{this.state.private}</div>
</div>
: null
}
<div className="group">
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
description="Label for user address (ID)" /></label>&nbsp;
<tt>{this.state.address}</tt>
</div>
<div className="group">
<BadgeList trustedBadges={this.state.trustedBadges} />
</div>
{this.state.description ?
<div className="group">
<label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
</label>
<div>{this.state.description}</div>
</div> : null}
</div>
<div className="hr" />
<div className="panel-form-row">
<label>
<FormattedMessage id="label_muting_topic" defaultMessage="Muted:"
description="Label for Muting/unmuting the topic" />
</label>
<CheckBox name="P" checked={this.state.muted} onChange={this.handleMuted} />
</div>
{this.state.archived ?
<div className="panel-form-row">
<label>
<FormattedMessage id="label_unarchive_topic" defaultMessage="Archived:"
description="Label for unarchiving the topic" />
</label>
<CheckBox name="archived" checked={true} onChange={this.handleUnarchive} />
</div>
:
null
}
<div className="hr" />
<div className="panel-form-row">
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onNavigate('security');}}>
<i className="material-icons">security</i>&nbsp;<FormattedMessage id="button_security"
defaultMessage="Security" description="Navigaton button for security panel." />
</a>
</div>
{this.state.groupTopic && this.state.sharer ?
<>
<div className="hr" />
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_group_members" defaultMessage="Group members:"
description="Section title or label" />
</label>
</div>
<div className="panel-form-row">
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
<i className="material-icons">person_add</i> &nbsp;<FormattedMessage id="button_add_members"
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
</a>
</div>
<FormattedMessage id="group_has_no_members" defaultMessage="No members"
description="Shown in place of group members">{
(no_members) => <ContactList
tinode={this.props.tinode}
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage={no_members}
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showMode={true}
noScroll={true}
onTopicSelected={this.handleMemberSelected}
showContextMenu={this.state.admin ? this.handleContextMenu : false}
/>
}</FormattedMessage>
</>
:
null
}
</div>
}
</div>
);
}
};
export default injectIntl(InfoView);
+95
View File
@@ -0,0 +1,95 @@
// Login form.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import VisiblePassword from '../widgets/visible-password.jsx';
export default class LoginView extends React.Component {
constructor(props) {
super(props);
this.state = {
login: props.login,
password: '',
hostName: props.serverAddress,
saveToken: props.persist
};
this.handleLoginChange = this.handleLoginChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleToggleSaveToken = this.handleToggleSaveToken.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleLoginChange(e) {
this.setState({login: e.target.value});
}
handlePasswordChange(e) {
this.setState({password: e.target.value});
}
handleToggleSaveToken() {
this.props.onPersistenceChange(!this.state.saveToken);
this.setState({saveToken: !this.state.saveToken});
}
handleSubmit(e) {
e.preventDefault();
this.props.onLogin(this.state.login.trim(), this.state.password.trim());
}
render() {
let submitClasses = 'primary';
if (this.props.disabled) {
submitClasses += ' disabled';
}
return (
<form id="login-form" onSubmit={this.handleSubmit}>
<FormattedMessage id="login_prompt" defaultMessage="Login"
description="Placeholer for username/login">
{
(login_prompt) => <input type="text" id="inputLogin"
placeholder={login_prompt}
autoComplete="username"
autoCorrect="off"
autoCapitalize="none"
value={this.state.login}
onChange={this.handleLoginChange}
required autoFocus />
}
</FormattedMessage>
<FormattedMessage id="password_prompt" defaultMessage="Password"
description="Placeholder/prompt for entering password">
{
(password_prompt) => <VisiblePassword type="password" id="inputPassword"
placeholder={password_prompt}
autoComplete="current-password"
value={this.state.password}
onChange={this.handlePasswordChange}
required={true} />
}
</FormattedMessage>
<div className="panel-form-row">
<CheckBox id="save-token" name="save-token" checked={this.state.saveToken}
onChange={this.handleToggleSaveToken} />
<label htmlFor="save-token">&nbsp;
<FormattedMessage id="stay_logged_in" defaultMessage="Stay logged in"
description="Label for a checkbox" />
</label>
<a href="#reset">
<FormattedMessage id="forgot_password_link" defaultMessage="Forgot password?"
description="Link to Reset password form" />
</a>
</div>
<div className="dialog-buttons">
<button className={submitClasses} type="submit">
<FormattedMessage id="button_sign_in" defaultMessage="Sign in"
description="Button [Sign In]" />
</button>
</div>
</form>
);
}
};
+27
View File
@@ -0,0 +1,27 @@
/* This is just a static page to display when no conversation is selected. */
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Tinode } from 'tinode-sdk';
import { APP_NAME } from '../config.js';
export default class LogoView extends React.PureComponent {
render() {
const version = APP_NAME + ' (' + Tinode.getLibrary() + ')';
return (
<div id="dummy-view">
<div>
<a href="https://github.com/tinode/chat/">
<img id="logo" alt="logo" src="img/logo.svg" />
<h2>Tinode Web</h2>
</a>
<p><FormattedMessage id="label_client" defaultMessage="Client:"
description="Label for a client version" /> {version}</p>
<p><FormattedMessage id="label_server" defaultMessage="Server:"
description="Label for a server version" /> {this.props.serverVersion} ({this.props.serverAddress})</p>
</div>
</div>
);
}
};
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
// Create new topic and invite users or send an invite.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Tinode } from 'tinode-sdk';
import ContactList from '../widgets/contact-list.jsx';
import NewTopicById from '../widgets/new-topic-by-id.jsx';
import NewTopicGroup from '../widgets/new-topic-group.jsx';
import SearchContacts from '../widgets/search-contacts.jsx';
import HashNavigation from '../lib/navigation.js';
import { theCard } from '../lib/utils.js';
const messages = defineMessages({
search_for_contacts: {
id: "search_for_contacts",
defaultMessage: "Use search to find contacts",
description: "Text shown in contacts view when user entered no search query."
},
search_no_results: {
id: "search_no_results",
defaultMessage: "Search returned no results",
description: "Text shown in contacts view when query returned no results."
},
search_placeholder: {
id: "search_placeholder",
defaultMessage: "List like email:alice@example.com, tel:17025550003...",
description: "Placeholder in contacts search field"
},
});
class NewTopicView extends React.Component {
constructor(props) {
super(props);
this.state = {
tabSelected: 'find',
searchQuery: null
};
this.handleTabClick = this.handleTabClick.bind(this);
this.handleSearchContacts = this.handleSearchContacts.bind(this);
this.handleSearchResultSelected = this.handleSearchResultSelected.bind(this);
this.handleNewGroupSubmit = this.handleNewGroupSubmit.bind(this);
this.handleGroupByID = this.handleGroupByID.bind(this);
}
componentDidMount() {
this.props.onInitFind();
}
handleTabClick(e) {
e.preventDefault();
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({tabSelected: e.currentTarget.dataset.id});
}
handleSearchContacts(query) {
this.props.onSearchContacts(query);
this.setState({searchQuery: Tinode.isNullValue(query) ? null : query});
}
handleSearchResultSelected(name) {
if (this.state.tabSelected == 'find') {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(name);
}
}
handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(undefined,
{public: theCard(name, dataUrl, null, description), private: priv, tags: tags}, isChannel);
}
handleGroupByID(topicName) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(topicName);
}
render() {
const {formatMessage} = this.props.intl;
const no_contacts_placeholder = formatMessage(this.state.searchQuery ?
messages.search_no_results : messages.search_for_contacts);
const search_placeholder = formatMessage(messages.search_placeholder);
return (
<div className="flex-column">
<ul className="tabbar">
<li className={this.state.tabSelected === "find" ? "active" : null}>
<a href="#" data-id="find" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_find_user" defaultMessage="find"
description="Tab title Find" />
</a>
</li>
<li className={this.state.tabSelected === "grp" ? "active" : null}>
<a href="#" data-id="grp" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_new_group" defaultMessage="new group"
description="Tab title New Group" />
</a>
</li>
<li className={this.state.tabSelected === "byid" ? "active" : null}>
<a href="#" data-id="byid" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_group_by_id" defaultMessage="by id"
description="Tab title Find topic by ID" />
</a>
</li>
</ul>
{this.state.tabSelected === 'grp' ?
<NewTopicGroup
tinode={this.props.tinode}
onSubmit={this.handleNewGroupSubmit}
onError={this.props.onError} /> :
this.state.tabSelected === 'byid' ?
<NewTopicById
onSubmit={this.handleGroupByID}
onError={this.props.onError} /> :
<div className="flex-column">
<SearchContacts
placeholder={search_placeholder}
onSearchContacts={this.handleSearchContacts} />
<ContactList
tinode={this.props.tinode}
contacts={this.props.searchResults}
myUserId={this.props.myUserId}
emptyListMessage={no_contacts_placeholder}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleSearchResultSelected} />
</div>}
</div>
);
}
};
export default injectIntl(NewTopicView);
+215
View File
@@ -0,0 +1,215 @@
// A password reset form.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import PhoneEdit from '../widgets/phone-edit.jsx';
import VisiblePassword from '../widgets/visible-password.jsx';
import HashNavigation from '../lib/navigation.js';
const messages = defineMessages({
password_reset_email_sent: {
id: 'password_reset_email_sent',
defaultMessage: 'An email with security code has been sent.',
description: 'Notification that the email with password reset instructions has been sent'
},
password_reset_sms_sent: {
id: 'password_reset_sms_sent',
defaultMessage: 'A text message with security code has been sent.',
description: 'Notification that the SMS with password reset instructions has been sent'
}
});
class PasswordResetView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
tel: '',
email: '',
password: '',
sent: false,
haveCode: false,
code: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleCodeChange = this.handleCodeChange.bind(this);
this.handleShowCodeField = this.handleShowCodeField.bind(this);
// Connection will trigger change by changing the this.props.serverVersion.
props.tinode.connect()
.catch(err => {
this.props.onError(err.message, 'err');
});
}
componentDidMount() {
const parsed = HashNavigation.parseUrlHash(window.location.hash);
this.setState({token: parsed.params.token, scheme: parsed.params.scheme});
}
handleSubmit(e) {
e.preventDefault();
if (this.state.token) {
// Reset using token.
this.props.onReset(this.state.password.trim(), {
scheme: 'token',
token: this.state.token
});
} else if (this.state.code && this.props.reqCredMethod) {
// Reset using security code.
// The secret is structured as <code>:<cred_method>:<cred_value>, "123456:email:alice@example.com".
const cred = this.state.email.trim() || this.state.tel.trim();
this.props.onReset(this.state.password.trim(), {
scheme: 'code',
secret: btoa(`${this.state.code}:${this.props.reqCredMethod}:${cred}`)
});
} else {
// Send email or SMS with the secret code.
const email = this.state.email.trim();
const tel = this.state.tel.trim();
this.setState({email: email, tel: tel});
this.props.onRequest(this.props.reqCredMethod, email || tel)
.then(_ => {
this.setState({sent: true});
const msg = this.props.reqCredMethod == 'email' ? messages.password_reset_email_sent :
this.props.reqCredMethod == 'tel' ? messages.password_reset_sms_sent : null;
if (msg) {
this.props.onError(this.props.intl.formatMessage(msg), 'info');
}
});
}
}
handleEmailChange(e) {
this.setState({email: e.target.value});
}
handlePasswordChange(e) {
this.setState({password: e.target.value});
}
handleCodeChange(e) {
this.setState({code: e.target.value.replace(/[^\d]/g, '')});
}
handleShowCodeField(e) {
e.preventDefault();
this.setState({haveCode: true});
}
render() {
const showCredentialInput = !(this.state.token && this.state.scheme);
const showPasswordInput = !showCredentialInput || this.state.haveCode || this.state.sent;
const passwordInput = (<>
<label className="small gray" htmlFor="new-password">
<FormattedMessage id="label_new_password"
defaultMessage="New password"
description="Label for entering a new password" />
</label>
<FormattedMessage id="new_password_placeholder" defaultMessage="Enter new password"
description="Placeholder for entering new password">{
(placeholder) => <VisiblePassword
id="new-password"
placeholder={placeholder}
autoComplete="new-password"
value={this.state.password}
required={true} autoFocus={true}
onChange={this.handlePasswordChange} />
}</FormattedMessage>
</>);
const emailInput = (<>
{this.state.haveCode ?
null :
<label htmlFor="inputEmail">
<FormattedMessage id="label_reset_password"
defaultMessage="Send a password reset email"
description="Label for password reset field" />
</label>
}
<FormattedMessage id="credential_email_prompt" defaultMessage="Your registration email"
description="Placeholder for entering email">{
(placeholder) => <input type="email" id="inputEmail"
placeholder={placeholder}
autoComplete="email"
value={this.state.email}
onChange={this.handleEmailChange}
required autoFocus />
}</FormattedMessage>
</>);
const phoneInput = (<>
{this.state.haveCode ?
null :
<div><label>
<FormattedMessage id="label_reset_password_tel"
defaultMessage="Send SMS to reset password"
description="Label for password reset field by phone" />
</label></div>
}
<div>
<label className="small gray">
<FormattedMessage id="mobile_phone_number" defaultMessage="Mobile phone number"
description="Prompt for entering a mobile phone number" />
</label>
</div>
<div className="panel-form-row">
<PhoneEdit
autoFocus={true}
onShowCountrySelector={this.props.onShowCountrySelector}
onSubmit={number => this.setState({tel: number})} />
</div>
</>);
const codeInput = (<>
<div>
<label className="small gray" htmlFor="enter-confirmation-code">
<FormattedMessage id="enter_confirmation_code_prompt"
defaultMessage="Confirmation code"
description="Request to enter confirmation code" />
</label>
</div>
<div>
<FormattedMessage id="numeric_confirmation_code_prompt"
defaultMessage="Numbers only" description="Prompt for numeric conformation code">{
(numbers_only) => <input type="text" id="enter-confirmation-code"
placeholder={numbers_only} maxLength={10}
value={this.state.code} onChange={this.handleCodeChange} required />
}</FormattedMessage>
</div>
</>);
const credentialInput = this.props.reqCredMethod == 'email' ?
emailInput : this.props.reqCredMethod == 'tel' ? phoneInput : null;
return (
<form id="password-reset-form" onSubmit={this.handleSubmit}>
{!this.state.sent && showCredentialInput ? credentialInput : null}
{this.state.haveCode || this.state.sent ? codeInput : null}
{showPasswordInput ? passwordInput : null}
<div className="dialog-buttons">
{this.state.haveCode || this.state.sent ? null :
<a href="#" onClick={this.handleShowCodeField} style={{marginRight: 'auto'}}>
<FormattedMessage id="password_i_have_code" defaultMessage="I have code"
description="Call to open field to enter password reset code" />
</a>
}
<button className="primary" type="submit">{
showPasswordInput ?
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" />
:
<FormattedMessage id="button_send_request" defaultMessage="Send request"
description="Button [Send request]" />
}</button>
</div>
</form>
);
}
}
export default injectIntl(PasswordResetView);
+98
View File
@@ -0,0 +1,98 @@
// Tinode config panel.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import HostSelector from '../widgets/host-selector.jsx';
export default class SettingsView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
transport: props.transport || 'def',
serverAddress: props.serverAddress,
secureConnection: props.secureConnection,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleTransportSelected = this.handleTransportSelected.bind(this);
this.handleServerAddressChange = this.handleServerAddressChange.bind(this);
this.handleToggleSecure = this.handleToggleSecure.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.props.onUpdate({
transport: this.state.transport,
serverAddress: this.state.serverAddress,
secureConnection: this.state.secureConnection,
});
}
handleTransportSelected(e) {
this.setState({transport: e.currentTarget.value});
}
handleServerAddressChange(name) {
this.setState({serverAddress: name});
}
handleToggleSecure(e) {
this.setState({secureConnection: !this.state.secureConnection});
}
render() {
const names = {def: "default", ws: "websocket", lp: "long polling"};
const transportOptions = [];
['def', 'ws', 'lp'].forEach(item => {
const id = 'transport-' + item;
const name = names[item];
transportOptions.push(
<li key={item}>
<input type="radio" id={id} name="transport-select" value={item}
checked={this.state.transport === item}
onChange={this.handleTransportSelected} />
<label htmlFor={id}>{name}</label>
</li>
);
});
return (
<form id="settings-form" className="panel-form" onSubmit={this.handleSubmit}>
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_server_to_use" defaultMessage="Server to use:"
description="Label for server selector in SettingsView" />
</label>
</div>
<HostSelector serverAddress={this.state.serverAddress} onServerAddressChange={this.handleServerAddressChange} />
<div className="panel-form-row">
<CheckBox id="secure-connection" name="secure-connection" checked={this.state.secureConnection}
className="quoted" onChange={this.handleToggleSecure} /><label htmlFor="secure-connection">
<FormattedMessage id="label_use_secure_connection" defaultMessage="Use secure connection"
description="Label for WS/WSS connection type in SettingsView" />
</label>
</div>
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_wire_transport" defaultMessage="Wire transport:"
description="Label for wire transport selection in SettingsView" />
</label>
</div>
<div className="panel-form-row">
<ul className="quoted">
{transportOptions}
</ul>
</div>
<div className="dialog-buttons">
<button type="submit" className="primary">
<FormattedMessage id="button_update" defaultMessage="Update"
description="Button [Update]" />
</button>
</div>
</form>
);
}
};
+275
View File
@@ -0,0 +1,275 @@
import React, { Suspense } from 'react';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ErrorPanel from '../widgets/error-panel.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx';
import SideNavbar from '../widgets/side-navbar.jsx';
import TopicCommonView from './topic-common-view.jsx';
const AccountSettingsView = React.lazy(_ => import('./account-settings-view.jsx'));
import ContactsView from './contacts-view.jsx';
const CreateAccountView = React.lazy(_ => import('./create-account-view.jsx'));
import AccNotificationsView from './acc-notifications-view.jsx';
import AccSecurityView from './acc-security-view.jsx';
import AccSupportView from './acc-support-view.jsx';
import LoginView from './login-view.jsx';
import NewTopicView from './new-topic-view.jsx';
const PasswordResetView = React.lazy(_ => import('./password-reset-view.jsx'));
import SettingsView from './settings-view.jsx';
import ValidationView from './validation-view.jsx';
// Panel titles for translation.
const messages = defineMessages({
login: {
id: 'sidepanel_title_login',
description: 'Sidepanel title for LoginView.',
defaultMessage: 'Sign In'
},
register: {
id: 'sidepanel_title_register',
description: 'Sidepanel title for CreateAccountView.',
defaultMessage: 'Create Account'
},
settings: {
id: 'sidepanel_title_settings',
description: 'Sidepanel title for SettingsView.',
defaultMessage: 'Settings'
},
edit: {
id: 'sidepanel_title_account_settings',
description: 'Sidepanel title for AccountSettingsView.',
defaultMessage: 'Account Settings'
},
general: {
id: 'panel_title_general',
description: 'Title for TopicCommon.',
defaultMessage: 'General'
},
security: {
id: 'panel_title_security',
description: 'Title for TopicSecirity and AccSecurity.',
defaultMessage: 'Security'
},
crop: {
id: 'panel_title_crop',
description: 'Title for AvatarCropView.',
defaultMessage: 'Drag to Adjust'
},
notif: {
id: 'sidepanel_title_acc_notifications',
description: 'Sidepanel title for AccNotificationsView.',
defaultMessage: 'Notifications'
},
support: {
id: 'sidepanel_title_acc_support',
description: 'Sidepanel title for AccSupportView.',
defaultMessage: 'Support'
},
newtpk: {
id: 'sidepanel_title_newtpk',
description: 'Sidepanel title for NewTopicView.',
defaultMessage: 'Start New Chat'
},
cred: {
id: 'sidepanel_title_cred',
description: 'Sidepanel title for ValidationView.',
defaultMessage: 'Confirm Credentials'
},
reset: {
id: 'sidepanel_title_reset',
description: 'Sidepanel title for PasswordResetView.',
defaultMessage: 'Reset Password'
},
archive: {
id: 'sidepanel_title_archive',
description: 'Sidepanel title for ContactsView-Archive.',
defaultMessage: 'Archived Chats'
},
blocked: {
id: 'sidepanel_title_blocked',
description: 'Sidepanel title for ContactsView-Blocked.',
defaultMessage: 'Blocked Chats'
}
});
class SidepanelView extends React.PureComponent {
constructor(props) {
super(props);
this.handleNewTopic = this.handleNewTopic.bind(this);
}
handleNewTopic() {
this.props.onNavigate('newtpk');
}
render() {
const {formatMessage} = this.props.intl;
const view = this.props.state || (this.props.myUserId ? 'contacts' : 'login');
let title, avatar, badges;
if (view == 'contacts') {
title = this.props.title;
avatar = this.props.avatar ? this.props.avatar : true;
badges = this.props.trustedBadges;
} else {
title = formatMessage(messages[view]);
avatar = false;
badges = null;
}
let onCancel;
if (['login', 'contacts'].indexOf(view) == -1) {
onCancel = this.props.onCancel;
}
return (
<div id="sidepanel">
<SideNavbar
state={view}
title={title}
avatar={avatar}
tinode={this.props.tinode}
trustedBadges={badges}
myUserId={this.props.myUserId}
onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings}
onNewTopic={this.handleNewTopic}
onCancel={onCancel} />
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
action={this.props.errorAction}
actionText={this.props.errorActionText}
onClearError={this.props.onError} />
<LoadSpinner show={this.props.loadSpinnerVisible} />
{view === 'login' ?
<LoginView
login={this.props.login}
disabled={this.props.loginDisabled}
persist={this.props.persist}
onLogin={this.props.onLoginRequest}
onPersistenceChange={this.props.onPersistenceChange} /> :
view === 'register' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<CreateAccountView
tinode={this.props.tinode}
reqCredMethod={this.props.reqCredMethod}
onShowCountrySelector={this.props.onShowCountrySelector}
onCreateAccount={this.props.onCreateAccount}
onCancel={this.props.onCancel}
onError={this.props.onError} />
</Suspense> :
view === 'settings' ?
<SettingsView
transport={this.props.transport}
serverAddress={this.props.serverAddress}
secureConnection={this.props.secureConnection}
onCancel={this.props.onCancel}
onUpdate={this.props.onGlobalSettings} /> :
view === 'edit' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<AccountSettingsView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
trustedBadges={this.props.trustedBadges}
onShowCountrySelector={this.props.onShowCountrySelector}
onNavigate={this.props.onNavigate}
onCredAdd={this.props.onCredAdd}
onCredDelete={this.props.onCredDelete}
onCredConfirm={this.props.onCredConfirm}
onError={this.props.onError} />
</Suspense> :
(view === 'general' || view === 'crop') ?
<TopicCommonView
topic="me"
tinode={this.props.tinode}
myUserId={this.props.myUserId}
reqCredMethod={this.props.reqCredMethod}
onUpdateTopicDesc={this.props.onUpdateAccountDesc}
onUpdateTagsRequest={this.props.onUpdateAccountTags}
onError={this.props.onError} /> :
view === 'notif' ?
<AccNotificationsView
messageSounds={this.props.messageSounds}
desktopAlerts={this.props.desktopAlerts}
desktopAlertsEnabled={this.props.desktopAlertsEnabled}
incognitoMode={this.props.incognitoMode}
onTogglePushNotifications={this.props.onTogglePushNotifications}
onToggleMessageSounds={this.props.onToggleMessageSounds}
onToggleIncognitoMode={this.props.onToggleIncognitoMode} /> :
view === 'security' ?
<AccSecurityView
tinode={this.props.tinode}
onUpdateAccountDesc={this.props.onUpdateAccountDesc}
onUpdatePassword={this.props.onUpdatePassword}
onLogout={this.props.onLogout}
onDeleteAccount={this.props.onDeleteAccount}
onShowAlert={this.props.onShowAlert}
onShowBlocked={this.props.onShowBlocked} /> :
view === 'support' ?
<AccSupportView
serverAddress={this.props.serverAddress}
serverVersion={this.props.serverVersion} /> :
(view === 'contacts' || view == 'archive' || view == 'blocked') ?
<ContactsView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
connected={this.props.connected}
topicSelected={this.props.topicSelected}
archive={view == 'archive'}
blocked={view == 'blocked'}
chatList={this.props.chatList}
showContextMenu={this.props.showContextMenu}
onTopicSelected={this.props.onTopicSelected}
onShowArchive={this.props.onShowArchive} /> :
view === 'newtpk' ?
<NewTopicView
tinode={this.props.tinode}
searchResults={this.props.searchResults}
onInitFind={this.props.onInitFind}
onSearchContacts={this.props.onSearchContacts}
onCreateTopic={this.props.onCreateTopic}
onError={this.props.onError} /> :
view === 'cred' ?
<ValidationView
credCode={this.props.credCode}
credMethod={this.props.credMethod}
credToken={this.props.credToken}
onSubmit={this.props.onValidateCredentials}
onCancel={this.props.onCancel} /> :
view === 'reset' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<PasswordResetView
tinode={this.props.tinode}
reqCredMethod={this.props.reqCredMethod}
onShowCountrySelector={this.props.onShowCountrySelector}
onRequest={this.props.onPasswordResetRequest}
onReset={this.props.onResetPassword}
onCancel={this.props.onCancel}
onError={this.props.onError} />
</Suspense> :
null}
</div>
);
}
};
export default injectIntl(SidepanelView);
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
// View for editing topic parameters (including 'me' topic).
import React from 'react';
import TopicDescEdit from '../widgets/topic-desc-edit.jsx';
import { arrayEqual } from '../lib/utils.js';
export default class TopicCommonView extends React.Component {
constructor(props) {
super(props);
const topic = this.props.tinode.getTopic(this.props.topic);
const acs = topic.getAccessMode();
this.state = {
tags: []
};
this.previousTagsUpdated = undefined;
this.onTagsUpdated = this.onTagsUpdated.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
}
// No need to separately handle component mount.
componentDidUpdate(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
if (topic.onTagsUpdated != this.onTagsUpdated) {
if (topic.getType() == 'grp') {
this.previousTagsUpdated = topic.onTagsUpdated;
topic.onTagsUpdated = this.onTagsUpdated;
} else {
this.previousTagsUpdated = undefined;
}
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onTagsUpdated = this.previousTagsUpdated;
}
// Server informs that the tags have been updated.
onTagsUpdated(tags) {
this.setState({tags: tags});
if (this.previousTagsUpdated && this.previousTagsUpdated != this.onTagsUpdated) {
this.previousTagsUpdated(tags);
}
}
// Request server to change tags.
handleTagsUpdated(tags) {
if (!arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
this.props.onUpdateTagsRequest(this.props.topic, tags);
}
}
render() {
return (
<div className="scrollable-panel">
<TopicDescEdit
tinode={this.props.tinode}
topic={this.props.topic}
onUpdateTopicDesc={this.props.onUpdateTopicDesc}
onUpdateTags={this.handleTagsUpdated}
onError={this.props.onError} />
</div>
);
}
};
+119
View File
@@ -0,0 +1,119 @@
// ValidationView: panel for confirming credentials, like email or phone.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
phone: {
id: 'phone_dative',
defaultMessage: 'phone',
description: "Dative case of 'phone', i.e. 'phone' in 'by phone'",
},
email: {
id: 'email_dative',
defaultMessage: 'email',
description: "Dative case of 'email', i.e. 'email' in 'by email'",
}
});
class ValidationView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
code: props.credCode || '',
codeReceived: props.credCode
};
this.handleCodeChange = this.handleCodeChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.credCode != prevState.codeReceived) {
return {
code: nextProps.credCode || '',
codeReceived: nextProps.credCode
};
}
return prevState;
}
componentDidMount() {
// Submit code automatically if it's received from the parent.
if (this.props.credCode) {
this.props.onSubmit(this.props.credMethod, this.props.credCode, this.props.credToken);
}
}
componentDidUpdate(prevProps, prevState) {
// Submit code automatically if it's received from the parent.
if (this.state.codeReceived && this.state.code && this.state.code != prevState.code) {
this.props.onSubmit(this.props.credMethod, this.state.code, this.props.credToken);
}
}
handleCodeChange(e) {
this.setState({code: e.target.value.replace(/[^\d]/g, '')});
}
handleKeyPress(e) {
if (e.key === 'Enter') {
this.handleSubmit(e);
} else if (e.key == 'Escape') {
this.handleCancel(e);
}
}
handleSubmit(e) {
e.preventDefault();
if (this.state.code && this.state.code.trim()) {
this.props.onSubmit(this.props.credMethod, this.state.code.trim(), this.props.credToken);
}
}
handleCancel(e) {
e.preventDefault();
this.props.onCancel();
}
render() {
const { formatMessage } = this.props.intl;
const methods = {'email': formatMessage(messages.email), 'tel': formatMessage(messages.phone)};
const method = methods[this.props.credMethod] || this.props.credMethod;
return (
<div className="panel-form">
<div className="panel-form-row">
<label className="small gray" htmlFor="enter-confirmation-code">
<FormattedMessage id="enter_confirmation_code_prompt"
defaultMessage="Confirmation code"
description="Request to enter confirmation code"
values={{method: method}} />
</label>
</div>
<div className="panel-form-row">
<FormattedMessage id="numeric_confirmation_code_prompt"
defaultMessage="Numbers only" description="Prompt for numeric conformation code">{
(numbers_only) => <input type="text" id="enter-confirmation-code"
placeholder={numbers_only}
value={this.state.code} onChange={this.handleCodeChange}
onKeyPress={this.handleKeyPress} required />
}</FormattedMessage>
</div>
<div className="dialog-buttons">
<button className="secondary" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
</button>
<button className="primary" onClick={this.handleSubmit}>
<FormattedMessage id="button_confirm" defaultMessage="Confirm" description="Button [Confirm]" />
</button>
</div>
</div>
);
}
};
export default injectIntl(ValidationView);
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
export default class Alert extends React.PureComponent {
render() {
return (<div className="alert-container">
<div className="alert">
<div className="title">{this.props.title}</div>
<div className="content">{this.props.content}</div>
<div className="dialog-buttons">
{this.props.onReject ?
<button className="outline" onClick={this.props.onReject}>
{this.props.reject || <FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />}
</button>
:
null
}
{this.props.onConfirm ?
<button className="primary" onClick={this.props.onConfirm}>
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
</button>
:
null
}
</div>
</div>
</div>);
}
};
+95
View File
@@ -0,0 +1,95 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import FileProgress from './file-progress.jsx';
import { bytesToHumanSize } from '../lib/strformat.js';
import { isUrlRelative, sanitizeUrl } from '../lib/utils.js';
export default class Attachment extends React.Component {
constructor(props) {
super(props);
this.state = {
downloader: null,
progress: 0
};
this.downloadFile = this.downloadFile.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
downloadFile(url, filename, mimetype) {
if (!url) {
this.props.onError("Invalid download URL '" + url + "'");
return;
}
const downloader = this.props.tinode.getLargeFileHelper();
this.setState({downloader: downloader});
downloader.download(url, filename, mimetype,
loaded => this.setState({progress: loaded / this.props.size}),
err => this.props.onError(err, 'err')
)
.then(_ => this.setState({downloader: null, progress: 0}))
.catch(err => {
if (err) {
this.props.onError("Error downloading file: " + err.message, 'err');
}
this.setState({downloader: null, progress: 0});
});
}
handleCancel() {
if (this.props.uploading) {
this.props.onCancelUpload();
} else if (this.state.downloader) {
this.state.downloader.cancel();
}
}
render() {
let filename = this.props.filename || 'file_attachment';
if (filename.length > 36) {
filename = filename.substr(0, 16) + '...' + filename.substr(-16);
}
let size = this.props.size > 0 ?
<span className="small gray">({bytesToHumanSize(this.props.size)})</span> :
null;
// Detect if the download URL is relative or absolute.
// If the URL is relative use LargeFileHelper to attach authentication
// credentials to the request.
let url, helperFunc;
if (!this.props.uploading && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
// Relative URL. Use download helper.
url = '#';
helperFunc = (e) => {
e.preventDefault();
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
};
} else {
url = sanitizeUrl(this.props.downloadUrl);
helperFunc = null;
}
const downloadWidget = <><i className="material-icons">file_download</i> <FormattedMessage id="save_attachment"
defaultMessage="save" description="Call to save an attachment" /></>;
return (
<div className="attachment">
<div><i className="material-icons big gray">insert_drive_file</i></div>
<div className="flex-column">
<div>{filename} {size}</div>
{this.props.uploading || this.state.downloader ?
<FileProgress progress={this.props.uploading ? this.props.progress : this.state.progress}
onCancel={this.handleCancel} />
:
<div>{url ?
<a href={url} download={this.props.filename} onClick={helperFunc}>{downloadWidget}</a>
:
<span className="light-gray">{downloadWidget}</span>
}</div>
}
</div>
</div>
);
}
};
+271
View File
@@ -0,0 +1,271 @@
// Audio recorder widget.
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { secondsToTime } from '../lib/strformat';
import { base64ToIntArray } from '../lib/blob-helpers';
// Make canvas bigger than the element size to reduce blurring.
const CANVAS_UPSCALING = 2.0;
// Thickness of a visualization bar.
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
// Spacing between two visualization bars.
const SPACING = 2 * CANVAS_UPSCALING;
// Color of histogram bars.
const BAR_COLOR = '#888A';
const BAR_COLOR_DARK = '#666C';
const THUMB_COLOR = '#444E';
// Minimum number of amplitude bars to draw.
const MIN_PREVIEW_LENGTH = 16;
const messages = defineMessages({
icon_title_play: {
id: 'icon_title_play',
defaultMessage: 'Play recording',
description: 'Icon tool tip for starting audio playback'
}
});
class AudioPlayer extends React.PureComponent {
constructor(props) {
super(props);
let preview = base64ToIntArray(this.props.preview);
if (!Array.isArray(preview) || preview.length < MIN_PREVIEW_LENGTH) {
preview = null;
}
this.state = {
canPlay: false,
playing: false,
currentTime: '0:00',
duration: this.props.duration > 0 ? secondsToTime(this.props.duration / 1000) : '-:--',
longMin: this.props.duration >= 600000,
preview: preview
};
this.initAudio = this.initAudio.bind(this);
this.initCanvas = this.initCanvas.bind(this);
this.resampleBars = this.resampleBars.bind(this);
this.visualize = this.visualize.bind(this);
this.handlePlay = this.handlePlay.bind(this);
this.handleSeek = this.handleSeek.bind(this);
this.handleError = this.handleError.bind(this);
this.audioPlayer = null;
this.viewBuffer = [];
this.canvasRef = React.createRef();
}
componentDidMount() {
if (this.props.src) {
this.initAudio();
}
this.initCanvas();
}
componentWillUnmount() {
if (this.audioPlayer) {
this.audioPlayer.oncanplay = null;
this.audioPlayer.ontimeupdate = null;
this.audioPlayer.onended = null;
this.audioPlayer.pause();
this.audioPlayer = null;
}
}
componentDidUpdate(prevProps) {
if (this.props.src != prevProps.src) {
this.initAudio();
}
if (this.props.preview != prevProps.preview) {
let preview = base64ToIntArray(this.props.preview);
if (!Array.isArray(preview) || preview.length < MIN_PREVIEW_LENGTH) {
preview = null;
}
this.setState({preview: preview}, this.initCanvas);
}
}
initAudio() {
this.audioPlayer = new Audio(this.props.src);
this.audioPlayer.oncanplay = _ => this.setState({canPlay: true});
this.audioPlayer.ontimeupdate = _ => this.setState({
currentTime: secondsToTime(this.audioPlayer.currentTime, this.state.longMin)
});
this.audioPlayer.onended = _ => {
this.audioPlayer.currentTime = 0;
this.setState({playing: false, currentTime: secondsToTime(0, this.state.longMin)})
}
}
initCanvas() {
// Force canvas aspect ratio to match one of the element + upscale canvas to reduce blurring.
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
this.canvasContext = this.canvasRef.current.getContext('2d');
this.canvasContext.lineCap = 'round';
this.viewBuffer = this.resampleBars(this.state.preview);
this.visualize();
}
// Draw amplitude of sound.
visualize() {
if (!this.canvasRef.current) {
return;
}
const width = this.effectiveWidth;
const height = this.canvasRef.current.height;
this.canvasContext.lineWidth = LINE_WIDTH;
const drawFrame = _ => {
if (!this.canvasRef.current || !this.audioPlayer) {
// The component is unmounted.
return;
}
this.canvasContext.clearRect(0, 0, this.canvasRef.current.width, height);
if (this.viewBuffer) {
if (this.state.playing) {
window.requestAnimationFrame(drawFrame);
}
// Current playback position.
const thumbAt = this.props.duration ?
Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * (width - LINE_WIDTH * 2) : -1;
// Draw amplitude bars.
this.canvasContext.beginPath();
this.canvasContext.strokeStyle = BAR_COLOR_DARK;
for (let i = 0; i < this.viewBuffer.length; i++) {
let x = 1 + i * (LINE_WIDTH + SPACING) + LINE_WIDTH * 0.5;
let y = this.viewBuffer[i] * height * 0.9;
const color = x < thumbAt ? BAR_COLOR_DARK : BAR_COLOR;
if (this.canvasContext.strokeStyle != color) {
this.canvasContext.stroke();
this.canvasContext.beginPath();
this.canvasContext.strokeStyle = color;
}
this.canvasContext.moveTo(x, (height - y) * 0.5);
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
}
// Actually draw the bars on canvas.
this.canvasContext.stroke();
// Draw thumb.
if (this.props.duration) {
this.canvasContext.beginPath();
this.canvasContext.arc(thumbAt + LINE_WIDTH * 2, height * 0.5, LINE_WIDTH * 2, 0, 2 * Math.PI);
this.canvasContext.fillStyle = THUMB_COLOR;
this.canvasContext.fill();
}
}
}
drawFrame();
}
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
resampleBars(original) {
const dstCount = ((this.canvasRef.current.width - SPACING) / (LINE_WIDTH + SPACING)) | 0;
// Remove extra padding on the right due to fractional bar which is not drawn.
this.effectiveWidth = dstCount * (LINE_WIDTH + SPACING) + SPACING;
if (!Array.isArray(original) || original.length == 0) {
return Array.apply(null, Array(dstCount)).map(_ => 0.01);
}
const factor = original.length / dstCount;
let amps = [];
let maxAmp = -1;
for (let i=0; i<dstCount; i++) {
let lo = (i * factor) | 0; // low bound;
let hi = ((i + 1) * factor) | 0; // high bound;
if (hi == lo) {
amps[i] = original[lo];
} else {
let amp = 0.0;
for (let j=lo; j<hi; j++) {
amp += original[j]
}
amps[i] = Math.max(0, amp / (hi - lo));
}
maxAmp = Math.max(amps[i], maxAmp);
}
if (maxAmp > 0) {
return amps.map(a => a / maxAmp);
}
return Array.apply(null, Array(dstCount)).map(_ => 0.01);
}
handlePlay(e) {
e.preventDefault();
if (!this.state.canPlay) {
return;
}
if (this.state.playing) {
this.audioPlayer.pause();
this.setState({playing: false});
} else {
this.audioPlayer.play();
this.setState({playing: true}, this.visualize);
}
}
handleError(err) {
console.error(err);
}
handleSeek(e) {
e.preventDefault();
if (e.target && this.props.duration) {
const rect = e.target.getBoundingClientRect();
const offset = (e.clientX - rect.left) / this.effectiveWidth * CANVAS_UPSCALING;
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime, this.state.longMin)});
if (!this.state.playing) {
this.visualize();
}
}
}
render() {
const playClass = 'material-icons' +
(this.props.short ? '' : ' large') +
(this.state.canPlay ? '' : ' disabled');
const play = (<a href="#" onClick={this.handlePlay} title={this.props.intl.formatMessage(messages.icon_title_play)}>
<i className={playClass}>{this.state.playing ? 'pause_circle' :
(this.state.canPlay ? 'play_circle' : 'not_interested')}</i>
</a>);
return (<div className="audio-player">{this.props.short ?
<>
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} />
{play}
</>
:
<>
{play}
<div>
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} />
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
</div>
</>
}
</div>);
}
}
export default injectIntl(AudioPlayer);
+388
View File
@@ -0,0 +1,388 @@
// Audio recorder widget.
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import AudioPlayer from './audio-player.jsx';
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=642012
// It adds duration and SeekHead to the webm record.
import fixWebmDuration from 'webm-duration-fix';
import { intArrayToBase64 } from '../lib/blob-helpers.js'
import { secondsToTime } from '../lib/strformat';
import { KEYPRESS_DELAY, MAX_DURATION, MIN_DURATION } from '../config.js';
// FFT resolution.
const BUFFER_SIZE = 256;
// Make canvas bigger than the element size to reduce blurring.
const CANVAS_UPSCALING = 2.0;
// Thickness of a visualization bar.
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
// Spacing between two visualization bars.
const SPACING = 2 * CANVAS_UPSCALING;
// Duration represented by one visualization bar.
const MILLIS_PER_BAR = 100;
// Color of histogram bars
const BAR_COLOR = '#BBBD';
// Vertical scaling for visualization bars.
const BAR_SCALE = 64.0;
// Number of bars in preview.
const VISUALIZATION_BARS = 96;
// Maximum number of samples per bar.
const MAX_SAMPLES_PER_BAR = 10;
// Recording format.
const AUDIO_MIME_TYPE = 'audio/webm';
const messages = defineMessages({
icon_title_delete: {
id: 'icon_title_delete',
defaultMessage: 'Delete recording',
description: 'Icon tool tip for deleting recorded audio'
},
icon_title_pause: {
id: 'icon_title_pause',
defaultMessage: 'Pause playback',
description: 'Icon tool tip for pausing audio playback'
},
icon_title_resume: {
id: 'icon_title_resume',
defaultMessage: 'Resume playback',
description: 'Icon tool tip for resuming audio playback'
},
icon_title_send: {
id: 'icon_title_send',
defaultMessage: 'Send message',
description: 'Icon tool tip for sending a message'
},
});
class AudioRecorder extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
enabled: true,
audioRecord: null,
recording: true,
paused: false,
duration: '0:00',
blobUrl: null,
preview: null
};
this.visualize = this.visualize.bind(this);
this.initMediaRecording = this.initMediaRecording.bind(this);
this.initCanvas = this.initCanvas.bind(this);
this.getRecording = this.getRecording.bind(this);
this.cleanUp = this.cleanUp.bind(this);
this.handleResume = this.handleResume.bind(this);
this.handlePause = this.handlePause.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleDone = this.handleDone.bind(this);
this.durationMillis = 0;
this.startedOn = null;
this.viewBuffer = [];
this.canvasRef = React.createRef();
// Timestamp for sending "recording" notifications.
this.recordingTimestamp = 0;
}
componentDidMount() {
this.stream = null;
this.mediaRecorder = null;
this.audioContext = null;
this.audioInput = null;
this.analyser = null;
this.audioChunks = [];
// Start recorder right away.
try {
navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(this.initMediaRecording, this.props.onError);
} catch (err) {
this.props.onError(err);
}
}
componentWillUnmount() {
this.startedOn = null;
if (this.stream) {
this.cleanUp();
}
}
// Draw amplitude of sound.
visualize() {
this.initCanvas();
const pcmData = new Uint8Array(this.analyser.frequencyBinCount);
const width = this.canvasWidth;
const height = this.canvasHeight;
// Number of bars.
const viewLength = (width / (LINE_WIDTH + SPACING)) | 0;
// Duration of audio which fits onto the screen.
const viewDuration = MILLIS_PER_BAR * viewLength;
this.canvasContext.lineWidth = LINE_WIDTH;
this.canvasContext.strokeStyle = BAR_COLOR;
let prevBarCount = 0;
let volume = 0.0;
let countPerBar = 0;
const drawFrame = _ => {
if (!this.startedOn) {
return;
}
window.requestAnimationFrame(drawFrame);
const duration = this.durationMillis + (Date.now() - this.startedOn);
// Update record length timer.
this.setState({duration: secondsToTime(duration / 1000)});
// Check if record is too long.
if (duration > MAX_DURATION) {
this.startedOn = null;
this.mediaRecorder.pause();
this.durationMillis += Date.now() - this.startedOn;
this.setState({enabled: false, recording: false, duration: secondsToTime(this.durationMillis / 1000)});
}
// Draw histogram.
// Get current waveform and calculate its amplitude.
this.analyser.getByteTimeDomainData(pcmData);
let amp = 0.0;
for (const amplitude of pcmData) {
amp += (amplitude - 127) ** 2;
}
// Sum the amplitude.
volume += Math.sqrt(amp/pcmData.length);
countPerBar ++;
let barCount = (duration / MILLIS_PER_BAR) | 0;
// Shift of the histogram along x-axis to make scrolling smooth. No need to shift if recording is too short.
const dx = viewDuration > duration ? 0 :
(duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
if (prevBarCount != barCount) {
prevBarCount = barCount;
// Add new amplitude visualization bar.
this.viewBuffer.push(volume/countPerBar);
volume = 0.0;
countPerBar = 0;
if (this.viewBuffer.length > viewLength) {
// Keep at most 'viewLength' amplitude bars.
this.viewBuffer.shift();
}
}
// Clear canvas.
this.canvasContext.clearRect(0, 0, width, height);
// Draw amplitude bars.
this.canvasContext.beginPath();
for (let i = 0; i < this.viewBuffer.length; i++) {
let x = i * (LINE_WIDTH + SPACING) - dx;
let y = Math.min(this.viewBuffer[i] / BAR_SCALE, 0.9) * height;
this.canvasContext.moveTo(x, (height - y) * 0.5);
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
}
// Actually draw the bars on canvas.
this.canvasContext.stroke();
// Send notification, if needed.
const now = new Date().getTime();
if (now - this.recordingTimestamp > KEYPRESS_DELAY) {
this.props.onRecordingProgress();
this.recordingTimestamp = now;
}
}
drawFrame();
}
handlePause(e) {
e.preventDefault();
this.mediaRecorder.pause();
this.mediaRecorder.requestData();
this.durationMillis += Date.now() - this.startedOn;
this.startedOn = null;
this.setState({recording: false});
}
handleResume(e) {
e.preventDefault();
if (this.state.enabled) {
this.startedOn = Date.now();
this.mediaRecorder.resume();
this.setState({recording: true}, this.visualize);
}
}
handleDelete(e) {
e.preventDefault();
this.durationMillis = 0;
this.startedOn = null;
this.mediaRecorder.stop();
this.cleanUp();
this.setState({recording: false});
}
handleDone(e) {
e.preventDefault();
this.setState({recording: false});
if (this.startedOn) {
this.durationMillis += Date.now() - this.startedOn;
this.startedOn = null;
}
// Stop recording and return data.
if (this.mediaRecorder) {
this.mediaRecorder.stop();
}
}
initCanvas() {
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
this.canvasContext = this.canvasRef.current.getContext('2d');
this.canvasContext.lineCap = 'round';
// To reduce line blurring.
this.canvasContext.translate(0.5, 0.5);
this.canvasWidth = this.canvasRef.current.width;
this.canvasHeight = this.canvasRef.current.height;
}
initMediaRecording(stream) {
this.stream = stream;
this.mediaRecorder = new MediaRecorder(stream, {mimeType: AUDIO_MIME_TYPE, audioBitsPerSecond: 24_000});
// The following code is needed for visualization.
this.audioContext = new AudioContext();
this.audioInput = this.audioContext.createMediaStreamSource(stream);
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = BUFFER_SIZE;
this.audioInput.connect(this.analyser);
this.mediaRecorder.onstop = _ => {
if (this.durationMillis > MIN_DURATION) {
this.getRecording(this.mediaRecorder.mimeType)
.then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
} else {
this.props.onDeleted();
}
this.cleanUp();
}
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
this.audioChunks.push(e.data);
}
if (this.mediaRecorder.state != 'inactive') {
this.getRecording(this.mediaRecorder.mimeType).then(result => {
this.setState({
blobUrl: result.url,
preview: result.preview
});
});
}
}
this.durationMillis = 0;
this.startedOn = Date.now();
this.mediaRecorder.start();
this.visualize();
this.props.onRecordingProgress();
this.recordingTimestamp = this.startedOn;
}
// Obtain data in a form sutable for sending or playing back.
// If duration is valid, apply fix for Chrome's WebM duration bug.
getRecording(mimeType) {
mimeType = mimeType || AUDIO_MIME_TYPE;
let blob = new Blob(this.audioChunks, {type: mimeType});
return fixWebmDuration(blob, mimeType)
.then(fixedBlob => { blob = fixedBlob; return fixedBlob.arrayBuffer(); })
.then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff))
.then(decoded => this.createPreview(decoded))
.then(preview => ({url: window.URL.createObjectURL(blob), preview: intArrayToBase64(preview)}));
}
// Preview must be calculated at the source: Chrome does not allow background AudioContext.
createPreview(audio) {
const data = audio.getChannelData(0);
// Number of amplitude bars in preview.
const viewLength = Math.min(data.length, VISUALIZATION_BARS);
// The number of samples in each bar.
const totalSPB = (data.length / viewLength) | 0;
// Distance between samples: we are going to take just a fracton of samples.
const samplingRate = Math.max(1, (totalSPB / MAX_SAMPLES_PER_BAR) | 0);
let buffer = [];
let max = -1;
for (let i = 0; i < viewLength; i++) {
let amplitude = 0;
let count = 0;
for (let j = 0; j < totalSPB; j += samplingRate) {
amplitude += data[totalSPB * i + j] ** 2;
count ++;
}
const val = Math.sqrt(amplitude / count);
buffer.push(val);
max = Math.max(max, val);
}
// Normalize amplitude to 0..100.
if (max > 0) {
buffer = buffer.map(a => (100 * a / max) | 0);
}
return buffer;
}
cleanUp() {
this.audioInput.disconnect();
this.stream.getTracks().forEach(track => track.stop());
}
render() {
const {formatMessage} = this.props.intl;
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
return (
<div className="audio">
<a href="#" onClick={this.handleDelete} title={formatMessage(messages.icon_title_delete)}>
<i className="material-icons gray">delete_outline</i>
</a>
{this.state.recording ?
<canvas ref={this.canvasRef} />
:
<AudioPlayer
src={this.state.blobUrl}
preview={this.state.preview}
duration={this.durationMillis}
short={true} />
}
<div className="duration">{this.state.duration}</div>
{this.state.recording ?
<a href="#" onClick={this.handlePause} title={formatMessage(messages.icon_title_pause)}>
<i className="material-icons">pause_circle_outline</i>
</a> :
<a href="#" onClick={this.handleResume} title={formatMessage(messages.icon_title_resume)}>
<i className={resumeClass}>radio_button_checked</i>
</a>
}
<a href="#" onClick={this.handleDone} title={formatMessage(messages.icon_title_send)}>
<i className="material-icons">send</i>
</a>
</div>
);
}
}
export default injectIntl(AudioRecorder);
+68
View File
@@ -0,0 +1,68 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Cropper from './cropper.jsx';
import { imageCrop } from '../lib/blob-helpers.js';
export default class AvatarCrop extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
top: 0,
left: 0,
width: 0,
height: 0,
scale: 1
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
// Cut out in image coordinates and desired scale factor.
handleChange(left, top, width, height, scale) {
this.setState({
left: left,
top: top,
width: width,
height: height,
scale: scale
});
}
handleSubmit() {
imageCrop(this.props.mime, this.props.avatar,
this.state.left, this.state.top, this.state.width, this.state.height,
this.state.scale)
.then(img => {
this.props.onSubmit(img.mime, img.blob, img.width, img.height);
})
.catch(err => {
this.props.onError(err);
});
}
render() {
return (
<div className="panel-form">
<div className="panel-form-row">
<Cropper
source={this.props.avatar}
onChange={this.handleChange} />
</div>
<div className="dialog-buttons">
{this.props.onCancel ?
<button className="secondary" onClick={this.props.onCancel}>
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
</button>
: null}
<button className="primary" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
</button>
</div>
</div>
);
}
};
+69
View File
@@ -0,0 +1,69 @@
import React from 'react';
import LetterTile from './letter-tile.jsx';
import LoadSpinner from './load-spinner.jsx';
import { sanitizeUrlForMime } from '../lib/utils.js';
import { AVATAR_SIZE } from '../config.js';
export default class AvatarUpload extends React.Component {
constructor(props) {
super(props);
this.state = {
source: props.avatar
};
this.handleFileReceived = this.handleFileReceived.bind(this);
}
componentDidUpdate(prevProps) {
if (this.props.avatar != prevProps.avatar) {
this.setState({source: this.props.avatar});
}
}
handleFileReceived(e) {
const image = e.target.files[0];
this.props.onImageUpdated(image.type, URL.createObjectURL(image), image.name);
// Clear the value so the same file can be uploaded again.
e.target.value = '';
}
render() {
// Randomize id value in case more than one AvatarUpload is shown
// at the same time.
const randId = 'file-input-avatar-' + ('' + Math.random()).substring(0, 4);
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
return (
<div className={className}>
{this.props.readOnly || !this.state.source ?
null :
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageUpdated();}}>
<i className="material-icons">clear</i>
</a>}
{this.state.source ?
<img src={this.props.tinode.authorizeURL(sanitizeUrlForMime(this.state.source, 'image'))} className="preview" /> :
this.props.readOnly && this.props.uid ?
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={true}
topic={this.props.uid}
title={this.props.title} />
</div>
:
<div className="blank">{AVATAR_SIZE}&times;{AVATAR_SIZE}</div>}
{this.props.readOnly ? null :
<input type="file" id={randId} className="inputfile hidden"
accept="image/*" onChange={this.handleFileReceived} />}
{this.props.readOnly ? null :
<label htmlFor={randId} className="round">
<i className="material-icons">file_upload</i>
</label>}
<LoadSpinner show={this.props.uploading} large={true} clear={true} centered={true} />
</div>
);
}
};
+43
View File
@@ -0,0 +1,43 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
const icon_mapping = {'staff': 'verified_user'};
const messages = defineMessages({
badge_verified: {
id: 'badge_verified',
defaultMessage: 'Verified/official',
description: 'Explanation of a verified account or topic badge'
},
badge_staff: {
id: 'badge_staff',
defaultMessage: 'Staff-managed',
description: 'Explanation of a staff-managed account or topic badge'
},
badge_danger: {
id: 'badge_danger',
defaultMessage: 'Untrustworthy',
description: 'Suspicious or untrustworthy account or topic badge'
},
});
/* Trusted badges with labels: verified, staff, danger */
class BadgeList extends React.PureComponent {
render() {
const {formatMessage} = this.props.intl;
let badges = null;
if (this.props.trustedBadges && this.props.trustedBadges.length > 0) {
badges = [];
this.props.trustedBadges.forEach(b => {
const comment = this.props.short ? null : formatMessage(messages['badge_' + b]);
const style = 'material-icons ' + b + '-color';
badges.push(<div className="trusted-badge" key={b}><i className={style}>{icon_mapping[b] || b}</i> {comment}</div>);
});
return <>{badges}</>;
}
return null;
}
};
export default injectIntl(BadgeList);
+12
View File
@@ -0,0 +1,12 @@
// The <- button to be displayed in title bars.
import React from 'react';
export default class ButtonBack extends React.PureComponent {
render() {
return (
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onBack();}}>
<i className="material-icons">arrow_back</i>
</a>
);
}
}
+151
View File
@@ -0,0 +1,151 @@
// IncomingCall: displays Accept & Reject buttons for incoming calls.
import React from 'react';
import BadgeList from './badge-list.jsx';
import LetterTile from './letter-tile.jsx';
import { MAX_TITLE_LENGTH, MAX_PEER_TITLE_LENGTH } from '../config.js';
import { CALL_STATE_INCOMING_RECEIVED } from '../constants.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { clipStr } from '../lib/utils.js'
const RING_SOUND = new Audio('audio/call-in.m4a');
export default class CallIncoming extends React.Component {
constructor(props) {
super(props);
this.state = {
topic: null,
fullName: undefined,
avatar: null,
trustedBadges: [],
previousMetaDesc: undefined,
};
this.resetDesc = this.resetDesc.bind(this);
this.onMetaDesc = this.onMetaDesc.bind(this);
this.handleRejectCall = this.handleRejectCall.bind(this);
this.handleAcceptCall = this.handleAcceptCall.bind(this);
this.ringTimer = null;
}
componentDidMount() {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetDesc(topic);
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
RING_SOUND.play().catch(_ => {});
this.ringTimer = setInterval(_ => {RING_SOUND.play().catch(_ => {})}, 2000);
this.props.onRinging(this.props.topic, this.props.seq);
}
}
// No need to separately handle component mount.
componentDidUpdate(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
if (this.onMetaDesc != topic.onMetaDesc) {
this.previousMetaDesc = topic.onMetaDesc;
topic.onMetaDesc = this.onMetaDesc;
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
this.resetDesc(topic, props);
}
}
componentWillUnmount() {
if (this.ringTimer != null) {
clearInterval(this.ringTimer);
RING_SOUND.pause();
}
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.setState({topic: null});
topic.onMetaDesc = this.previousMetaDesc;
}
resetDesc(topic) {
const badges = [];
if (topic.trusted) {
for (const [key, val] of Object.entries(topic.trusted)) {
if (val) {
badges.push(key);
}
}
}
this.setState({
fullName: clipStr(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
trustedBadges: badges,
});
}
onMetaDesc(desc) {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetDesc(topic);
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
this.previousMetaDesc(desc);
}
}
handleAcceptCall() {
this.props.onAcceptCall(this.props.topic);
}
handleRejectCall() {
this.props.onReject(this.props.topic, this.props.seq);
this.props.onClose();
}
render() {
return (
<div className="alert-container">
<div className="incoming-call">
<div className="caller-card incoming pulse">
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={this.state.avatar || true}
topic={this.props.topic}
title={this.state.fullName} />
</div>
<div className="caller-name">{clipStr(this.state.fullName, MAX_PEER_TITLE_LENGTH)}
<BadgeList short={true} trustedBadges={this.state.trustedBadges} /></div>
</div>
<div className="controls">
{this.props.callState == CALL_STATE_INCOMING_RECEIVED ?
<>
<button className="danger" onClick={this.handleRejectCall}>
<i className="material-icons">call_end</i>
</button>
<button className="positive" onClick={this.handleAcceptCall}>
<i className="material-icons">call</i>
</button>
</>
:
null
}
</div>
</div>
</div>
);
}
};
+47
View File
@@ -0,0 +1,47 @@
// Chat message describing a video or voice call.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { secondsToTime } from '../lib/strformat.js';
export default class CallMessage extends React.PureComponent {
render() {
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green');
const callIcon = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
let duration;
if (isCallDropped) {
switch (this.props.callState) {
case 'busy':
duration = <FormattedMessage id="call_busy" defaultMessage="busy" description="Label for busy call line" />;
break;
case 'declined':
duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />;
break;
case 'missed':
duration = this.props.incoming ?
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for missed incoming call" /> :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />;
break;
default:
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
break;
}
} else {
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
}
return <div className="call-message">
<div><i className="material-icons big gray">call</i></div>
<div className="flex-column narrow">
<div>{this.props.incoming ?
<FormattedMessage id="calls_incoming" defaultMessage="Incoming call" description="Incoming call label" /> :
<FormattedMessage id="calls_outgoing" defaultMessage="Outgoing call" description="Outgoing call label" />
}</div>
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
</div>
</div>;
}
}
+649
View File
@@ -0,0 +1,649 @@
// CallPanel displays call in progress: local and remote viewports and controls.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import LetterTile from './letter-tile.jsx';
import { MAX_PEER_TITLE_LENGTH } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
import { clipStr } from '../lib/utils.js'
const RING_SOUND = new Audio('audio/call-out.m4a');
RING_SOUND.loop = true;
const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
CALL_ENDED_SOUND.loop = true;
const DIALING_SOUND = new Audio('audio/dialing.m4a');
const VIDEO_MUTED_EVENT = 'video:muted';
const VIDEO_UNMUTED_EVENT = 'video:unmuted';
const messages = defineMessages({
already_in_call: {
id: 'already_in_call',
defaultMessage: 'You already in an ongoing call!',
description: 'Error message when the user tried to accept a new call without finishing pervious one',
}
});
class CallPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
localStream: undefined,
remoteStream: undefined,
pc: undefined,
dataChannel: undefined,
previousOnInfo: undefined,
waitingForPeer: false,
// If true, the client has received a remote SDP from the peer and has sent a local SDP to the peer.
callInitialSetupComplete: false,
audioOnly: props.callAudioOnly,
// Video mute/unmute in progress.
videoToggleInProgress: false,
// Indicates if the remote peer has informed us that their camera is on.
remoteVideoLive: false,
};
this.localStreamConstraints = {
audio: true,
video: !props.callAudioOnly
};
this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED;
this.localRef = React.createRef();
this.remoteRef = React.createRef();
// Cache for remote ice candidates until initial setup gets completed.
this.remoteIceCandidatesCache = [];
this.onInfo = this.onInfo.bind(this);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.createPeerConnection = this.createPeerConnection.bind(this);
this.canSendOffer = this.canSendOffer.bind(this);
this.drainRemoteIceCandidatesCache = this.drainRemoteIceCandidatesCache.bind(this);
this.handleNegotiationNeededEvent = this.handleNegotiationNeededEvent.bind(this);
this.handleICECandidateEvent = this.handleICECandidateEvent.bind(this);
this.handleNewICECandidateMsg = this.handleNewICECandidateMsg.bind(this);
this.handleICEConnectionStateChangeEvent = this.handleICEConnectionStateChangeEvent.bind(this);
this.handleSignalingStateChangeEvent = this.handleSignalingStateChangeEvent.bind(this);
this.handleICEGatheringStateChangeEvent = this.handleICEGatheringStateChangeEvent.bind(this);
this.handleIceCandidateErrorEvent = this.handleIceCandidateErrorEvent.bind(this);
this.handleTrackEvent = this.handleTrackEvent.bind(this);
this.handleVideoOfferMsg = this.handleVideoOfferMsg.bind(this);
this.handleVideoAnswerMsg = this.handleVideoAnswerMsg.bind(this);
this.handleNewICECandidateMsg = this.handleNewICECandidateMsg.bind(this);
this.reportError = this.reportError.bind(this);
this.handleGetUserMediaError = this.handleGetUserMediaError.bind(this);
this.stopTracks = this.stopTracks.bind(this);
this.disconnectMedia = this.disconnectMedia.bind(this);
this.handleCloseClick = this.handleCloseClick.bind(this);
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
this.muteVideo = this.muteVideo.bind(this);
this.unmuteVideo = this.unmuteVideo.bind(this);
this.emptyVideoTrack = this.emptyVideoTrack.bind(this);
this.handleDataChannelEvent = this.handleDataChannelEvent.bind(this);
this.handleDataChannelError = this.handleDataChannelError.bind(this);
this.handleDataChannelMessage = this.handleDataChannelMessage.bind(this);
this.handleDataChannelOpen = this.handleDataChannelOpen.bind(this);
this.handleDataChannelClose = this.handleDataChannelClose.bind(this);
}
componentDidMount() {
const topic = this.props.tinode.getTopic(this.props.topic);
this.previousOnInfo = topic.onInfo;
topic.onInfo = this.onInfo;
if ((this.props.callState == CALL_STATE_OUTGOING_INITATED ||
this.props.callState == CALL_STATE_IN_PROGRESS) && this.localRef.current) {
this.start();
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onInfo = this.previousOnInfo;
this.stop();
}
handleVideoCallAccepted(info) {
RING_SOUND.pause();
// Create peer connection with a data channel.
const pc = this.createPeerConnection(true);
const stream = this.state.localStream;
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
if (track.kind == 'video' && this.state.audioOnly) {
// This is an audio-only call.
// Remove dummy video track (placeholder remains).
track.enabled = false;
track.stop();
stream.removeTrack(track);
}
});
}
onInfo(info) {
if (info.what != 'call') {
return;
}
switch (info.event) {
case 'accept':
this.handleVideoCallAccepted(info);
break;
case 'answer':
this.handleVideoAnswerMsg(info);
break;
case 'ice-candidate':
this.handleNewICECandidateMsg(info);
break;
case 'hang-up':
this.handleRemoteHangup(info);
break;
case 'offer':
this.handleVideoOfferMsg(info);
break;
case 'ringing':
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
RING_SOUND.play().catch(_ => {});
break;
default:
console.warn("Unknown call event", info.event);
break;
}
}
// Creates an empty video track placeholder.
emptyVideoTrack() {
const width = 640;
const height = 480;
const canvas = Object.assign(document.createElement("canvas"), {width, height});
canvas.getContext('2d').fillRect(0, 0, width, height);
const stream = canvas.captureStream(0);
return Object.assign(stream.getVideoTracks()[0], {enabled: false});
}
start() {
if (this.state.localStream) {
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
return;
}
if (this.props.callState == CALL_STATE_IN_PROGRESS) {
// We apparently just accepted the call.
this.props.onInvite(this.props.topic, this.props.seq, CALL_STATE_IN_PROGRESS, this.props.callAudioOnly);
return;
}
// This is an outgoing call waiting for the other side to pick up.
// Start local video.
navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
.then(stream => {
if (!this.localStreamConstraints.video) {
// Starting an audio-only call. Create a dummy video track
// (so video can be enabled during the call if the user desires).
stream.addTrack(this.emptyVideoTrack());
}
this.setState({localStream: stream, waitingForPeer: true});
this.localRef.current.srcObject = stream;
DIALING_SOUND.play();
// Send call invitation.
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly);
})
.catch(this.handleGetUserMediaError);
}
stop() {
CALL_ENDED_SOUND.pause();
CALL_ENDED_SOUND.currentTime = 0;
RING_SOUND.pause();
RING_SOUND.currentTime = 0;
this.stopTracks(this.state.localStream);
this.stopTracks(this.state.remoteStream);
this.disconnectMedia(this.localRef.current);
this.disconnectMedia(this.remoteRef.current);
if (this.state.pc) {
this.state.pc.ontrack = null;
this.state.pc.onremovetrack = null;
this.state.pc.onremovestream = null;
this.state.pc.onicecandidate = null;
this.state.pc.oniceconnectionstatechange = null;
this.state.pc.onsignalingstatechange = null;
this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null;
this.state.pc.onicecandidateerror = null;
this.state.pc.ondatachannel = null;
if (this.state.dataChannel && (this.state.dataChannel.readyState == 'open' || this.state.dataChannel.readyState == 'connecting')) {
this.state.dataChannel.close();
}
this.state.pc.close();
}
this.setState({pc: null, waitingForPeer: false});
}
disconnectMedia(el) {
if (!el) {
return;
}
el.srcObject = null;
el.src = '';
}
stopTracks(stream) {
if (!stream) {
return;
}
let tracks = stream.getTracks();
if (tracks) {
tracks.forEach(track => {
track.stop();
track.enabled = false;
});
}
}
handleDataChannelError(error) {
console.error('data channel error', error);
}
handleDataChannelMessage(event) {
switch (event.data) {
case VIDEO_MUTED_EVENT:
this.setState({remoteVideoLive: false}, _ => { this.remoteRef.current.srcObject = this.state.remoteStream; });
break;
case VIDEO_UNMUTED_EVENT:
this.setState({remoteVideoLive: true}, _ => { this.remoteRef.current.srcObject = this.state.remoteStream; });
break;
default:
break;
}
}
handleDataChannelOpen(event) {
if (!this.state.audioOnly) {
event.target.send(VIDEO_UNMUTED_EVENT);
}
}
handleDataChannelClose(event) {
console.log('close data channel:', event);
}
handleDataChannelEvent(event) {
console.log('data channel event:', event);
const channel = event.channel;
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose;
this.setState({dataChannel: channel});
}
createPeerConnection(withDataChannel) {
const iceServers = this.props.tinode.getServerParam('iceServers', null);
const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection();
pc.onicecandidate = this.handleICECandidateEvent;
pc.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent;
pc.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent;
pc.onsignalingstatechange = this.handleSignalingStateChangeEvent;
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
pc.ontrack = this.handleTrackEvent;
pc.ondatachannel = this.handleDataChannelEvent;
let stateUpdate = {pc: pc, waitingForPeer: false};
if (withDataChannel) {
// Create data channel for exchanging events.
const channel = pc.createDataChannel("events", {ordered: true});
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose;
stateUpdate.dataChannel = channel;
}
this.setState(stateUpdate);
return pc;
}
handleVideoAnswerMsg(info) {
// Configure the remote description, which is the SDP payload
// in 'info' message.
const desc = new RTCSessionDescription(info.payload);
this.state.pc.setRemoteDescription(desc)
.then(_ => {
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
})
.catch(err => this.reportError(err));
}
reportError(err) {
this.props.onError(err.message, 'err');
}
canSendOffer() {
return this.isOutgoingCall || this.state.callInitialSetupComplete;
}
handleNegotiationNeededEvent(event) {
const pc = event.target;
if (!this.canSendOffer()) {
return;
}
pc.createOffer().then(offer => {
return pc.setLocalDescription(offer);
})
.then(_ => {
this.props.onSendOffer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
})
.catch(err => this.reportError(err));
}
handleIceCandidateErrorEvent(event) {
console.warn("ICE candidate error:", event);
}
handleICECandidateEvent(event) {
if (event.candidate) {
this.props.onIceCandidate(this.props.topic, this.props.seq, event.candidate.toJSON());
}
}
handleNewICECandidateMsg(info) {
const candidate = new RTCIceCandidate(info.payload);
if (this.state.callInitialSetupComplete) {
this.state.pc.addIceCandidate(candidate)
.catch(err => {
if (candidate.candidate) {
this.reportError(err);
}
console.warn("Error adding new ice candidate", candidate, err);
});
} else {
this.remoteIceCandidatesCache.push(candidate);
}
}
drainRemoteIceCandidatesCache() {
this.remoteIceCandidatesCache.forEach(candidate => {
this.state.pc.addIceCandidate(candidate)
.catch(err => {
if (candidate.candidate) {
this.reportError(err);
}
console.warn("Error adding cached ice candidate", candidate, err);
});
});
this.remoteIceCandidatesCache = [];
}
handleICEConnectionStateChangeEvent(event) {
switch (event.target.iceConnectionState) {
case 'closed':
case 'failed':
this.handleCloseClick();
break;
}
}
handleSignalingStateChangeEvent(event) {
if (event.target.signalingState == 'closed') {
this.handleCloseClick();
}
}
handleICEGatheringStateChangeEvent(event) {
// ICE gathering change state
}
handleTrackEvent(event) {
// Remote video becomes available.
this.remoteRef.current.srcObject = event.streams[0];
this.setState({remoteStream: event.streams[0]});
}
handleGetUserMediaError(e) {
console.error("Error opening camera and/or microphone", e);
switch(e.name) {
case 'NotFoundError':
// Cannot start the call b/c no camera and/or microphone found.
this.reportError(e);
break;
case 'SecurityError':
case 'PermissionDeniedError':
// Do nothing; this is the same as the user canceling the call.
break;
default:
this.reportError(e);
break;
}
// Make sure we shut down our end of the RTCPeerConnection so we're
// ready to try again.
this.handleCloseClick();
}
handleVideoOfferMsg(info) {
let localStream = null;
// If needed, create peer connection.
// Not creating data channel since it should be created by the peer.
const pc = this.state.pc ? this.state.pc : this.createPeerConnection(false);
const desc = new RTCSessionDescription(info.payload);
pc.setRemoteDescription(desc).then(_ => {
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
})
.then(stream => {
let dummyVideo;
if (!this.localStreamConstraints.video) {
// Starting an audio-only call. Create an empty video track so
// so the user can enable the video during the call.
dummyVideo = this.emptyVideoTrack();
stream.addTrack(dummyVideo);
}
localStream = stream;
this.localRef.current.srcObject = stream;
this.setState({localStream: stream});
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
if (dummyVideo) {
dummyVideo.enabled = false;
dummyVideo.stop();
stream.removeTrack(dummyVideo);
}
})
.then(_ => {
return pc.createAnswer();
})
.then(answer => {
return pc.setLocalDescription(answer);
})
.then(_ => {
this.props.onSendAnswer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
})
.catch(this.handleGetUserMediaError);
}
// Call disconnected by remote.
handleRemoteHangup() {
if (!this.state.waitingForPeer) {
// This is live call, just hang up.
this.handleCloseClick();
} else {
// This is a call which is not yet connected.
// Stop pulse animation.
this.setState({waitingForPeer: false});
// Change sound and wait a bit before ending it.
RING_SOUND.pause();
RING_SOUND.currentTime = 0;
CALL_ENDED_SOUND.loop = true;
CALL_ENDED_SOUND.play().catch(_ => {});
setTimeout(_ => {
this.handleCloseClick();
}, 2000);
}
}
handleCloseClick() {
this.stop();
this.props.onHangup(this.props.topic, this.props.seq);
}
// Ends video track and turns off the camera.
muteVideo() {
if (!this.state.pc || !this.state.dataChannel) {
return;
}
const stream = this.state.localStream;
const t = stream.getVideoTracks()[0];
t.enabled = false;
t.stop();
stream.removeTrack(t);
this.state.dataChannel.send(VIDEO_MUTED_EVENT);
this.setState({videoToggleInProgress: false});
}
unmuteVideo() {
if (!this.state.pc || !this.state.dataChannel) {
return;
}
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
// Will extract video track from stream and throw stream away,
// and replace video track in the media sender.
this.localRef.current.srcObject = null;
const sender = this.state.pc.getSenders().find(s => s.track.kind == 'video');
const track = stream.getVideoTracks()[0];
// Remote track from new stream.
stream.removeTrack(track);
// Add this track to the existing local stream.
this.state.localStream.addTrack(track);
return sender.replaceTrack(track);
})
.then(_ => {
this.localRef.current.srcObject = this.state.localStream;
this.state.dataChannel.send(VIDEO_UNMUTED_EVENT);
})
.catch(err => this.handleGetUserMediaError(err))
.finally(_ => { this.setState({videoToggleInProgress: false}); }); // Make sure we redraw the mute/unmute icons (e.g. camera -> camera_off).
}
handleToggleCameraClick() {
if (this.state.videoToggleInProgress) {
// Toggle currently in progress.
return;
}
const tracks = this.state.localStream.getVideoTracks();
this.setState({videoToggleInProgress: true}, _ => {
if (tracks && tracks.length > 0 && tracks[0].enabled && tracks[0].readyState == 'live') {
this.muteVideo();
} else {
this.unmuteVideo();
}
this.setState({audioOnly: !this.state.audioOnly});
});
}
handleToggleMicClick() {
const stream = this.state.localStream;
const t = stream.getAudioTracks()[0];
t.enabled = !t.enabled;
// Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off).
this.forceUpdate();
}
render() {
const audioTracks = this.state.localStream && this.state.localStream.getAudioTracks();
const videoTracks = !this.state.audioOnly && this.state.localStream && this.state.localStream.getVideoTracks();
const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
const audioIcon = audioTracks && audioTracks[0] && audioTracks[0].enabled ? 'mic' : 'mic_off';
const videoIcon = videoTracks && videoTracks[0] && videoTracks[0].enabled && videoTracks[0].readyState == 'live' ? 'videocam' : 'videocam_off';
const peerTitle = clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
const pulseAnimation = this.state.waitingForPeer ? ' pulse' : '';
let remoteActive = false;
if (this.remoteRef.current && this.remoteRef.current.srcObject && this.state.remoteVideoLive) {
const rstream = this.remoteRef.current.srcObject;
if (rstream.getVideoTracks().length > 0) {
const t = rstream.getVideoTracks()[0];
remoteActive = t.enabled && t.readyState == 'live';
}
}
return (
<>
<div id="video-container">
<div id="video-container-panel">
<div className="call-party self" disabled={this.state.audioOnly}>
<video ref={this.localRef} autoPlay muted playsInline />
<div className="caller-name inactive">
<FormattedMessage id="calls_you_label"
defaultMessage="You" description="Shown over the local video screen" />
</div>
</div>
<div className="call-party peer" disabled={!remoteActive}>
{remoteActive ?
<>
<video ref={this.remoteRef} autoPlay playsInline />
<div className="caller-name inactive">{peerTitle}</div>
</> :
<>
<audio ref={this.remoteRef} autoPlay />
<div className={`caller-card${pulseAnimation}`}>
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={this.props.avatar}
topic={this.props.topic}
title={this.props.title} />
</div>
<div className="caller-name">{peerTitle}</div>
</div>
</>
}
</div>
</div>
<div className="controls">
<button className="danger" onClick={this.handleCloseClick}>
<i className="material-icons">call_end</i>
</button>
<button className="secondary" onClick={this.handleToggleCameraClick} disabled={disabled}>
<i className="material-icons">{videoIcon}</i>
</button>
<button className="secondary" onClick={this.handleToggleMicClick} disabled={disabled}>
<i className="material-icons">{audioIcon}</i>
</button>
</div>
</div>
</>
);
}
};
export default injectIntl(CallPanel);
+47
View File
@@ -0,0 +1,47 @@
// The counter of unread messages in the topic.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { secondsToTime } from '../lib/strformat.js';
// Props:
// incoming: true | false.
// success: true | false.
export default class CallStatus extends React.PureComponent {
render() {
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
const icon2 = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
let duration;
if (isCallDropped) {
switch (this.props.callState) {
case 'busy':
duration = <FormattedMessage id="call_busy" defaultMessage="busy" description="Label for busy call line" />;
break;
case 'declined':
duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />;
break;
case 'missed':
duration = this.props.incoming ?
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for missed incoming call" /> :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />;
break;
default:
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
break;
}
} else if (['accepted', 'started'].includes(this.props.callState) && !this.props.duration) {
duration = <FormattedMessage id="call_in_progress" defaultMessage="in progress" description="Label for call in progress" />
} else {
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
}
return (
<>
<div className="composed-material">
<i className="material-icons">call</i><i className="material-icons second">{icon2}</i>
</div> {duration}
</>
);
}
};
+241
View File
@@ -0,0 +1,241 @@
// Single chat message bubble, sent or received.
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Drafty, Tinode } from 'tinode-sdk';
import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx'
import { fullFormatter } from '../lib/formatters.js';
import { sanitizeUrl } from '../lib/utils.js';
class BaseChatMessage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
progress: 0
};
if (props.uploader) {
props.uploader.onProgress = this.handleProgress.bind(this);
}
this.handleExpandImage = this.handleExpandImage.bind(this);
this.handlePlayVideo = this.handlePlayVideo.bind(this);
this.handleFormButtonClick = this.handleFormButtonClick.bind(this);
this.handleContextClick = this.handleContextClick.bind(this);
this.handleCancelUpload = this.handleCancelUpload.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.formatterContext = {
formatMessage: props.intl.formatMessage.bind(props.intl),
viewportWidth: props.viewportWidth,
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
onImagePreview: this.handleExpandImage,
onVideoPreview: this.handlePlayVideo,
onFormButtonClick: this.handleFormButtonClick,
onQuoteClick: this.handleQuoteClick
};
}
handleExpandImage(e) {
e.preventDefault();
this.props.onExpandMedia({
url: e.target.src,
filename: e.target.dataset.name,
width: e.target.dataset.width,
height: e.target.dataset.height,
size: e.target.dataset.size,
type: e.target.dataset.mime
});
}
handlePlayVideo(e) {
e.preventDefault();
this.props.onExpandMedia({
video: true,
url: e.target.dataset.src,
preview: e.target.src,
filename: e.target.dataset.name,
width: e.target.dataset.width,
height: e.target.dataset.height,
duration: e.target.dataset.duration,
size: e.target.dataset.size,
type: e.target.dataset.mime
});
}
handleFormButtonClick(e) {
e.preventDefault();
const data = {
seq: this.props.seq
};
data.resp = {};
if (e.target.dataset.name) {
data.resp[e.target.dataset.name] = e.target.dataset.val ? e.target.dataset.val :
e.target.dataset.val === undefined ? 1 : '' + e.target.dataset.val;
}
if (e.target.dataset.act == 'url') {
data.ref = sanitizeUrl(e.target.dataset.ref) || 'about:blank';
}
const text = e.target.dataset.title || 'unknown';
this.props.onFormResponse(e.target.dataset.act, text, data);
}
handleContextClick(e) {
e.preventDefault();
e.stopPropagation();
const menuItems = [];
if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
menuItems.push('menu_item_send_retry');
}
if (this.props.received > Tinode.MESSAGE_STATUS_FATAL) {
if (this.props.userIsWriter) {
menuItems.push('menu_item_reply');
if (!this.props.response) {
let immutable = false;
Drafty.entities(this.props.content, (_0, _1, tp) => {
immutable = ['AU', 'EX', 'FM', 'IM', 'VC', 'VD'].includes(tp);
return immutable;
});
if (!immutable) {
Drafty.styles(this.props.content, tp => {
immutable = ['QQ'].includes(tp);
return immutable;
});
}
if (!immutable) {
menuItems.push('menu_item_edit');
}
}
}
if (this.props.userIsAdmin) {
menuItems.push(this.props.pinned ? 'menu_item_unpin' : 'menu_item_pin');
}
}
menuItems.push('menu_item_forward');
this.props.showContextMenu({
seq: this.props.seq,
replace: this.props.edited ? parseInt(this.props.edited.split(':')[1]) : 0,
content: this.props.content,
userFrom: this.props.userFrom,
userName: this.props.userName,
y: e.pageY,
x: e.pageX,
pickReply: this.props.pickReply,
editMessage: this.props.editMessage,
}, menuItems);
}
handleProgress(ratio) {
this.setState({progress: ratio});
}
handleCancelUpload() {
this.props.onCancelUpload(this.props.seq, this.props.uploader);
}
handleQuoteClick(e) {
e.preventDefault();
e.stopPropagation();
const replyToSeq = this.props.replyToSeq;
if (replyToSeq) {
this.props.onQuoteClick(replyToSeq);
}
}
render() {
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
const avatar = this.props.userAvatar || true;
const fullDisplay = (this.props.isGroup && this.props.response &&
(this.props.sequence == 'single' || this.props.sequence == 'last'));
let content = this.props.content;
const attachments = [];
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
Drafty.attachments(content, (att, i) => {
if (att.mime == 'application/json') {
// Don't show json objects as attachments.
// They are not meant for users.
return;
}
attachments.push(<Attachment
tinode={this.props.tinode}
downloadUrl={Drafty.getDownloadUrl(att)}
filename={att.name}
uploading={Drafty.isProcessing(att)}
mimetype={att.mime}
size={Drafty.getEntitySize(att)}
progress={this.state.progress}
onCancelUpload={this.handleCancelUpload}
onError={this.props.onError}
key={i} />);
}, this);
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
content = React.createElement(React.Fragment, null, tree);
} else if (typeof content != 'string') {
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
<FormattedMessage id="invalid_content"
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i></>
}
return (
<li ref={this.props.innerRef} className={sideClass}>
{this.props.isGroup && this.props.response ?
<div className="avatar-box">
{fullDisplay ?
<LetterTile
tinode={this.props.tinode}
topic={this.props.userFrom}
title={this.props.userName}
avatar={avatar} /> :
null}
</div> :
null}
<div>
<div className={bubbleClass}>
<div className="content-meta">
<div className="message-content">
{content}
{attachments}
</div>
{this.props.timestamp ?
<ReceivedMarker
edited={this.props.edited}
timestamp={this.props.timestamp}
received={this.props.received} />
: null}
</div>
{this.props.showContextMenu ?
<span className="menuTrigger">
<a href="#" onClick={this.handleContextClick}>
<i className="material-icons">expand_more</i>
</a>
</span> : null
}
</div>
{fullDisplay ?
<div className="author">
{this.props.userName ||
<i><FormattedMessage id="user_not_found" defaultMessage="Not found"
description="In place of a user's full name when the user is not found." /></i>
}
</div>
: null
}
</div>
</li>
);
}
};
const IntlChatMessage = injectIntl(BaseChatMessage);
const ChatMessage = React.forwardRef((props, ref) => <IntlChatMessage innerRef = {ref} {...props} />);
export default ChatMessage;

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