Compare commits

..
846 Commits
Author SHA1 Message Date
or-else 1a884d2546 version set to 0.25.0-beta1 2025-10-15 14:52:09 +03:00
or-else 16e688d4c1 fix CallPannel: do not remount between menimized/full screen 2025-10-10 14:58:01 +03:00
or-else 16bfaddf2c relabel as beta-1 2025-10-08 17:30:31 +03:00
or-else 1085d1caf6 allow messaging while call is in progress (same topic only for now) 2025-10-08 16:49:45 +03:00
or-else abf5b19758 translations added 2025-10-06 21:02:41 +03:00
or-else 44e2fc3a23 rebuilt with the sdk changes 2025-09-30 14:41:02 +03:00
or-else e505b0a607 minor visual adjustments 2025-09-29 20:52:17 +03:00
or-else 8ca7f5cd3e support for showing the number of subscribers 2025-09-29 20:51:51 +03:00
or-else a74ad9dd63 adjust pinned topic background in dark mode 2025-09-24 19:41:20 +03:00
or-else c70e5ebf7e fix late initialization of InfoView component 2025-09-24 19:38:29 +03:00
or-else 679276c2e2 ignore large size of devel modules 2025-09-24 19:37:42 +03:00
or-else 5faacc1f53 pinning topics work as expected 2025-09-23 17:08:34 +03:00
or-else 4818ee7a25 support for pinning topics; truncate text with support for RTL languages 2025-09-20 18:18:01 +03:00
or-else 0fb5132d24 fix error color in dark mode 2025-09-14 17:50:35 +03:00
or-else ed6be52228 merge master 2025-09-05 16:36:03 +03:00
or-else 1dc5ea1655 ux improvemnt: use contact name as title in alert dialog 2025-09-05 16:34:27 +03:00
or-else 6e04638441 make min image zoom non-zero 2025-09-01 14:51:09 +03:00
or-else 89e9b85551 cropper: fix incorrect image positioning after zoom-out 2025-08-21 10:13:43 +03:00
or-else 5bb829a6bc fix slow panning in image cropper 2025-08-20 20:58:07 +03:00
or-else 92611680e4 actually clear uploaded image 2025-08-20 20:57:36 +03:00
or-else 78f31b7cf1 minor visual tweaks 2025-08-20 20:57:09 +03:00
or-else 79a1bc98b2 add config option to send on Enter vs send on CTRL-Enter 2025-08-10 15:25:05 +03:00
or-else 0b05d480c7 change fixed color to light-dark 2025-08-10 12:18:43 +03:00
or-else 7d1011400e merge devel 2025-08-10 12:15:14 +03:00
or-else 3a4794bacb merge master 2025-08-10 10:33:20 +03:00
or-else 56790262f3 rename drafty form response content type to text/x-drafy-fr 2025-08-10 10:30:47 +03:00
or-else fa436b61be remove unused css style 2025-08-09 14:45:14 +03:00
or-else bb1a3f4cea some colors fixed in doc preview 2025-08-09 09:43:40 +03:00
or-else 36cafbe87b dependencies updated 2025-08-09 09:15:18 +03:00
or-else a84a5a677a bump version to 0.24.3 and rebuild 2025-08-07 16:13:47 +03:00
or-else 6bec95e084 automatically change mime-type of json files to application/octeat-stream 2025-08-07 16:12:51 +03:00
or-else 290571f305 transition to application/json+drafty as mime type for form responses 2025-08-07 15:39:43 +03:00
or-else 7ce9470bf3 more consistent jest time mocks 2025-07-30 18:12:55 +03:00
or-else 9eba65b711 italian translation 2025-07-30 17:20:08 +03:00
or-else 0400c804aa dependencies updated, version bumped, rebuilt 2025-07-25 11:44:06 +03:00
or-else 7ea1a24857 more unit tests added 2025-07-25 11:43:45 +03:00
or-else 80d3f5336b dependencies updated, rebuilt, version bumped to 24.1 2025-07-09 15:22:38 +03:00
or-else 6a208cf040 rebuilt 2025-07-09 10:54:31 +03:00
or-else 9cef61d447 color tweaks, support for text size adjustment 2025-07-09 10:54:18 +03:00
or-else 2cb9f69a9d option for adjusting text size, rearranged config components 2025-07-09 10:53:32 +03:00
or-else 276f2bed34 make some components lazy-loaded 2025-07-09 10:51:27 +03:00
or-else 2f05cd0651 translations updated for new features 2025-07-09 10:49:43 +03:00
or-else 1ba3671873 missing files added, minor color tweaks 2025-06-14 10:51:20 +03:00
or-else 6d02cc903c dark mode mostly done 2025-06-13 16:26:48 +03:00
or-else ff60b3902e bump version and rebuild 2025-06-08 16:54:42 +03:00
or-else e641dc0d32 automatically add dir="rtl" to <html> tag when the language is RTL 2025-06-06 10:41:10 +03:00
or-else 8535da995f disable tag editing for user profiles 2025-06-03 19:30:53 +03:00
or-else baad48f044 added missing arabic translations 2025-06-03 18:38:03 +03:00
or-else 72009a693b tweak alias query, update dependencies, change version to -rc3 2025-06-02 17:05:49 +03:00
or-else b3adf15f12 merge devel with arabic support 2025-05-28 17:31:59 +03:00
or-else e8b48dfd84 hook up arabic translation, rebuild 2025-05-28 13:05:16 +03:00
or-else 7c6fb38b02 rebuilt 2025-05-28 12:56:59 +03:00
or-else 28f332f56a cosmetic fixes, rebuilt 2025-05-28 12:56:17 +03:00
or-else 6fdc208ca7 crash due to incorrect reference to topic 2025-05-28 12:55:59 +03:00
Gene 2f74357aac Merge pull request #137 from lamoboos223/devel
added Arabic localization
2025-05-28 12:54:38 +03:00
Lama Alosaimi e692f80bcf removed (,) from service-worker.js 2025-05-24 13:37:34 +03:00
Lama Alosaimi a411b80fc2 added arabic localization 2025-05-24 13:35:46 +03:00
or-else 837013e97c alias cleanup and fixes 2025-05-09 19:19:02 +03:00
or-else 360b37ae13 version updated to 0.24.0-rc2 2025-05-06 11:36:40 +03:00
or-else c69229934e dependencies updated 2025-05-06 11:33:32 +03:00
or-else 8288ad4733 fix alias regex: allow only alphanum as the first char 2025-05-06 10:43:59 +03:00
or-else 7d627edfce make background dark when viewing images 2025-05-01 15:05:42 +03:00
or-else 04279395bc clean up alias validator timer 2025-04-23 21:09:47 +03:00
or-else 14b38af8da merge devel 2025-04-14 09:54:36 +03:00
or-else 3ae00646c7 rebuit with fixed sdk 2025-04-14 09:24:50 +03:00
or-else 43fde20643 fix unread count for topics with all messages deleted 2025-04-10 16:20:33 +03:00
or-else 6527c067c6 all translations done; 2025-03-12 21:11:31 +03:00
or-else 2f374e52ca translations: de, es, fr, ro, ru, uk 2025-03-12 20:23:33 +03:00
or-else b1acfecd4f fixing translations 2025-03-12 19:40:33 +03:00
or-else 5448f10b1a cosmetic alias-related bugs 2025-03-09 10:28:35 +03:00
or-else 3b78971a39 fix menu [delete for all] 2025-03-06 16:41:25 +03:00
or-else 11436a80ee support fro unique aliases, https://github.com/tinode/chat/issues/446 2025-03-04 13:05:51 +03:00
or-else 3567f323c7 add tests for lib/utils.js 2025-03-03 21:07:40 +03:00
or-else 6f05fd102f merge devel 2025-02-23 11:05:57 +03:00
or-else 4897d63029 bug fix: incorrect QR code in topic info 2025-02-23 11:05:00 +03:00
or-else 5e92189b97 ability to hard-delete p2p messages, debugged pinned messages 2025-02-22 18:55:53 +03:00
or-else 30bf77d4ba sending messages to self works 2025-02-16 22:06:02 +03:00
or-else b2772924bd more vc cleanup 2025-02-15 20:41:52 +03:00
or-else 6c1c8cf336 removed incomplete video conferences 2025-02-14 21:40:44 +03:00
or-else f91d8a979d implement touch swiping pinned messages 2025-02-14 17:38:49 +03:00
or-else 17b91c4bb8 prevent accidental clicks from tiggering scroll 2025-02-09 18:46:36 +03:00
or-else a3a4fe7505 dependencies updated, do not update react to 19 - buggy 2025-02-08 21:54:45 +03:00
or-else 02e8bd2f74 merge devel 2025-02-08 19:50:34 +03:00
or-else bc8f20977e changed browserslist config, improved browser sleep detection 2025-02-07 20:59:45 +03:00
or-else 12a97cacc8 rebuild for 0.23.0 release 2025-01-16 18:11:49 +03:00
or-else 4052bb72b6 dependencies updated, version bumped 2025-01-16 17:36:36 +03:00
or-else 458b519845 reconnect when PC wakes up from sleep 2024-12-14 20:44:44 +03:00
or-else 07a22564c2 add thai and ukrainian language codes to package.json 2024-05-30 16:12:14 -07:00
or-else 31a95676b7 version bumped to 23.0-rc1, dependencies updated 2024-05-30 16:09:28 -07:00
or-else ba81261ee8 rebuilt with correct sdk 2024-05-18 18:07:14 -07:00
or-else 944cf32bb9 Merge branch 'master' into devel 2024-05-08 15:23:42 -07:00
or-else ed34a46865 make emoji-only messages larger for 1-5 emojis 2024-05-08 13:39:10 -07:00
or-else a4befc4cd3 make font larger for emoji-only messages 2024-05-01 14:36:18 -07:00
or-else 7b15cf1f19 Merge branch 'devel' 2024-04-29 15:53:05 -07:00
or-else df03509029 bump version to 22.13, dependencies updated 2024-04-29 15:17:46 -07:00
or-else 354ce3b8a3 bug fix: do not alter data or blob URLs with [asatt] 2024-02-10 11:33:36 -08:00
or-else 66192a8d53 google decided that assentlins have to be updated 2024-02-08 13:41:11 -08:00
or-else 7f2a89d86c version bumped to 22.12, dependencies updated 2024-02-05 18:51:02 -08:00
or-else 9eaa172030 add QR code display 2024-02-02 16:26:49 -08:00
or-else f932b973f8 ensure asatt=1 is aded to image download URL to set Content-Disposition: attachment 2023-12-21 16:06:19 -08:00
or-else 40916e2fc3 repo url fixed 2023-12-15 15:32:04 -08:00
or-else 5faedc35f3 bump version, update dependencies, and rebuild 2023-12-15 14:27:32 -08:00
or-else de32da49f3 make sure only safe schemes are clickable in URLs 2023-12-15 14:27:05 -08:00
or-else f5714b3b69 fix a security issue with absolute URL appearing as relative du to backslashes 2023-12-15 14:25:44 -08:00
or-else 1155888ced dependencies updated 2023-10-23 17:30:17 -07:00
or-else d128cec90d add Thai and (missing) Ukrainian loaders, mention Thai translation 2023-09-25 14:13:17 -07:00
Gene 3a8ee3e2dd Merge pull request #135 from Bukharney/master
Added Thai translation
2023-09-25 14:05:31 -07:00
Jirapat Lakma 1164d3a5bd added Thai translation 2023-09-25 15:34:10 +07:00
Jirapat Lakma fb2a39280a add Thai translation 2023-09-25 11:25:59 +07:00
or-else a1faba6b0c bump version to 0.22.10 2023-08-16 09:30:40 +03:00
or-else ca24b82f54 mention ukrainian translation 2023-08-14 16:41:12 +03:00
or-else 395edb3e4c rebuilt with ukrainian translations 2023-08-14 16:37:20 +03:00
or-else 691879b2b2 ukrainian translations 2023-08-14 16:36:01 +03:00
or-else 8187f09d26 rebuilt with the fix for #131 2023-08-14 11:15:09 +03:00
or-else e82951722d Merge branch 'devel' of https://github.com/tinode/webapp into devel 2023-08-13 09:59:27 +03:00
or-else 1042b9c39a rebuilt with fix for #129, dependencies updated 2023-08-13 09:59:20 +03:00
Gene 3177b667b8 Merge pull request #130 from tandr3w/master
Fix sending of "read" notifications after backing out of a group on mobile
2023-08-12 09:16:32 +03:00
Andrew 5fb85ce822 Fix sending of "read" notifications after backing out of a group on mobile.
https://groups.google.com/g/tinode/c/DMmeN79dZe0
2023-08-11 15:46:46 -04:00
or-else 500d72702a update dependencies, bump version 2023-07-08 08:44:12 -07:00
Gene 31b13401a2 Merge pull request #125 from tinode/vc
Video conferencing skeleton.
2023-06-29 08:13:13 -07:00
aforge 338e0f6023 Merge branch 'vc' of https://github.com/tinode/webapp into vc 2023-06-29 03:57:12 -07:00
or-else df4b9c312a fix height of title panel in image preview 2023-06-27 19:06:29 -07:00
or-else 07d32dbfd8 fix unnecessary horizontal scrolling 2023-06-15 15:40:08 -07:00
or-else 0d887b73a0 changed border color of the current speaker 2023-06-15 15:31:07 -07:00
or-else 685a1cd2a4 remove this.participants, clone object instead 2023-06-15 15:20:39 -07:00
or-else 8f9f41efe5 removed unnecessary React component <> 2023-06-15 14:03:04 -07:00
or-else dde4e565c8 allow longer caller names 2023-06-15 13:11:56 -07:00
or-else 94575b5a07 pass viewport to call panel 2023-06-15 13:11:23 -07:00
or-else 3bf822170c clean up scrolling of carousel 2023-06-15 12:06:39 -07:00
or-else deca04ed3e visual style fixes for video confs 2023-06-14 18:46:49 -07:00
or-else 97908ec84b move refs from state to object, bug fix 2023-06-14 10:11:24 -07:00
or-else 20dcb48ec5 all remaining translations added 2023-06-10 19:52:03 -07:00
or-else 7bad07f717 UI work on conference calls, translations 2023-06-10 19:00:52 -07:00
or-else 2f85f01a1a make livekit external 2023-06-10 18:10:42 -07:00
or-else a86d07f854 merge next 2023-06-10 12:14:59 -07:00
or-else 414129d968 comments corrected 2023-06-10 12:00:16 -07:00
or-else 5d664b86cb merge devel 2023-06-09 17:16:22 -07:00
or-else d6c904d38a dependencies updated and rebuilt 2023-06-08 19:02:25 -07:00
Gene ab04a872e2 Merge pull request #128 from sun2008mt/master
fix Error: ENOENT: no such file or directory, open './src/i18n.min/src\i18n\zh.json' on windows platform
2023-05-29 09:24:45 -07:00
marc 7f0a16d701 fix Error: ENOENT: no such file or directory, open './src/i18n.min/src\i18n\zh.json' on windows platform 2023-05-29 23:09:11 +08:00
or-else 43e89e2119 update dependencies and rebuild 2023-05-25 13:44:46 -07:00
or-else baf211ff82 fix for /tinode/chat/issues/861 2023-05-17 18:31:26 -07:00
or-else c5c55fd9fd force message view refresh on subscription update, #127 2023-05-12 13:40:37 -07:00
or-else 8403e3e9b4 fix calculation of visible range 2023-05-11 16:57:22 -07:00
or-else fd08d19939 fix very long processing time in scroll handler 2023-05-11 15:43:31 -07:00
aforge b14717bb48 Merge branch 'next' into vc 2023-05-10 16:36:33 -07:00
or-else f969c04c47 npm audit fix 2023-05-10 14:44:59 -07:00
or-else 2fe35f9ded merge devel 2023-05-10 14:43:49 -07:00
or-else 852a1a7c4e fix for https://github.com/tinode/chat/issues/858 2023-05-10 13:20:40 -07:00
aforge f7120d824d Some style fixes. 2023-05-09 14:23:26 -07:00
or-else c15092e850 work in progress on disappearing deleted topic 2023-05-09 14:23:21 -07:00
or-else 7ca827559b provide default value for the field 2023-05-08 19:07:35 -07:00
or-else f7beabacf9 fix for https://github.com/tinode/chat/issues/861 2023-05-08 18:45:44 -07:00
or-else 848bb19f76 bump version 2023-05-08 17:19:32 -07:00
aforge 981ef684a7 Add livekit-client dep. 2023-05-08 17:01:42 -07:00
aforge 55fc4356da Remove '2' from VCPanel event handler names. 2023-05-08 16:52:08 -07:00
or-else c17118d157 fix a bunch of problems with credential validation 2023-05-08 14:08:21 -07:00
or-else 95e9891825 add extra spacing in error panel 2023-05-08 10:59:46 -07:00
or-else 0984c73c01 rebuilt with updated sdk 2023-05-06 10:42:52 -07:00
aforge 0425bd4348 Video conferencing skeleton. 2023-05-03 22:50:51 -07:00
or-else f9e7a71920 fix display of pinned messages 2023-05-02 10:28:30 -07:00
or-else ba72fe7602 rebuilt with correct sdk 2023-04-28 17:18:28 -07:00
or-else 6da591beb1 remove unused missingRanges 2023-04-28 17:16:38 -07:00
or-else 16c9e2bb23 dependencies updated, rebuilt 2023-04-28 13:08:57 -07:00
or-else 3689b5d527 another fix for #124 2023-04-28 12:48:16 -07:00
or-else c277c16a53 mostly functional 2023-04-27 17:44:20 -07:00
or-else 1bc3e15da3 merge devel 2023-04-27 11:43:30 -07:00
or-else 3ac895ec66 rebuilt with correct sdk version 2023-04-27 11:41:11 -07:00
or-else 08bdc7bc08 allow automatic subscription to a new p2p topic 2023-04-27 11:00:22 -07:00
or-else 03a137da4a fractional fetching, wip 2023-04-27 09:41:49 -07:00
or-else 8df6413705 merge devel 2023-04-25 12:34:25 -07:00
or-else 5d56c17128 another fix for #124 2023-04-25 12:31:07 -07:00
or-else 87c74f9396 rebuilt with sdk 2023-04-25 10:18:44 -07:00
or-else 2b7c80ef2d removed unused files 2023-04-24 14:36:05 -07:00
or-else 823215aea6 merge devel 2023-04-24 14:34:25 -07:00
or-else 5ad734cdea some new files added 2023-04-24 14:32:21 -07:00
or-else 48405bcb94 fix for #124 2023-04-24 14:25:48 -07:00
or-else 0f9b88e64e sdk with fractional message fetching (wip) 2023-04-24 11:38:23 -07:00
or-else 10a827504b always load pinned messages 2023-04-18 17:07:38 -07:00
or-else 1d24e9135b merge devel, update dependencies, rebuild 2023-04-16 11:47:47 -07:00
or-else a5c190f6db sync version with the backend 0.22.6 2023-04-15 10:43:33 -07:00
or-else f61f68d2ac added another missing translation 2023-04-14 20:21:40 -07:00
or-else 2c5705c3ae translations updated 2023-04-14 20:12:26 -07:00
or-else c4547bbeb4 partial update of translations 2023-04-14 19:56:23 -07:00
or-else e7296d5d14 rebuilt 2023-04-14 19:38:21 -07:00
or-else 364cd2c488 merge devel 2023-04-14 19:34:27 -07:00
or-else 855f871041 rebuilt with SDK fix for downloads 2023-04-14 19:20:25 -07:00
or-else 8dd32a341b fix some issues with audio recording and playback in safari #121, #122 2023-04-14 17:47:35 -07:00
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 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
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 775915e6b2 merging devel 2021-08-22 10:54:32 -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
aforge 9626995b1c Merge branch 'devel' into features 2021-08-20 09:25:15 +03: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 fb3de109e0 merge devel 2021-08-16 16:06:32 -07:00
or-else 982b40d6e8 mereg devel 2021-08-16 11:52:41 -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
aforge 1e08211d17 Merge branch 'devel' into features 2021-08-13 09:49:39 +03: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 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 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 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 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 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 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
266 changed files with 67077 additions and 24113 deletions
+3 -7
View File
@@ -1,8 +1,4 @@
index.dev.js binary
index.dev.js.map binary
index.prod.js binary
index.prod.js.map binary
base.min.css binary
tinode.dev.js binary
tinode.prod.js binary
umd/* binary
package-lock.json binary
*.min.css binary
src/i18n.min/* binary
+1 -1
View File
@@ -32,7 +32,7 @@ If applicable, add screenshots to help explain your problem.
- TinodeWeb Version [e.g. 0.16.6]
**Console log**
Please attach or insert console log which illustrates the problem.
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.
+2 -1
View File
@@ -1,3 +1,4 @@
.DS_Store
node_modules
src/i18n/src/
src/i18n/srci/
webapp.code-workspace.code-workspace
+3 -2
View File
@@ -4,7 +4,7 @@
"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"]
["25:9A:71:EE:BD:2E:9F:D3:2D:64:1E:90:49:33:BD:F3:E3:42:BD:C4:DD:FA:BA:68:EE:D2:EB:76:65:33:DC:92"]
}
},
{
@@ -22,4 +22,5 @@
"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"]
}
}]
}
]
+26 -20
View File
@@ -8,7 +8,7 @@ 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://web.tinode.co/.
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>
@@ -16,27 +16,43 @@ For demo access and other instructions [see here](https://github.com/tinode/chat
## 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).
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 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=fr.
The app is currently available in the following languages:
* English
* Chinese (simplified)
* Chinese (traditional)
* English - default
* Arabic
* Chinese simplified
* Chinese traditional
* French
* German
* Italian
* Korean
* Romanian
* Russian
* Spanish
* Thai
* Ukrainian
More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp).
@@ -46,7 +62,9 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
* End-to-End encryption.
* Emoji support is weak.
* Mentions, hashtags.
* Previews not generated for videos, audio, links or docs.
* Previews not generated for links and docs.
* Location and contact sharing.
* Recording video messages.
## Other
@@ -55,18 +73,6 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
* [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/.
## 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).
## Responsive design
### Desktop screenshot
@@ -78,5 +84,5 @@ If you want to use the app with your own server and want web push notification t
### 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-new-chat.png" alt="Mobile web: start new chat" width=250 /></kbd> <kbd><img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 /></kbd> <kbd><img src="web-mob-avatar.png" alt="Mobile web: change avatar" width=250 /></kbd>
<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.
BIN
View File
Binary file not shown.
+1642 -513
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -9,5 +9,7 @@ const FIREBASE_INIT = {
// 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"
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: 19 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

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.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg height="24" viewBox="0 -960 960 960" width="24" fill="#5f6368" xmlns="http://www.w3.org/2000/svg"><path d="M197-706l-84-85 56-57 85 85zM791-112l-85-85 57-57 84 85zM0-440v-80h120v80zM840-440v-80h120v80zM169-113l-57-56 85-85 57 57zM763-706l-57-57 85-84 57 56zM440 0v-120h80V0zM440-840v-120h80v120zM743-380a766.09 766.09 0 01-39.5-11c-13-4-25.833-8.667-38.5-14 5.333-12 9.167-24.167 11.5-36.5 2.333-12.333 3.5-25.167 3.5-38.5 0-55.333-19.5-102.5-58.5-141.5S535.333-680 480-680c-13.333 0-26.167 1.167-38.5 3.5-12.333 2.333-24.5 6.167-36.5 11.5-5.333-12.667-9.833-25.333-13.5-38a1098.034 1098.034 0 01-10.5-39c16-6 32.333-10.5 49-13.5s33.667-4.5 51-4.5c78 0 144.167 27.167 198.5 81.5S761-558 761-480c0 17.333-1.5 34.333-4.5 51s-7.5 33-13.5 49z" fill="orange"/><path d="M453-452c34 34 72 63.167 114 87.5s87 43.167 135 56.5c-26.667 34-59.333 60.5-98 79.5S524.333-200 481-200c-78 0-144.167-27.167-198.5-81.5S201-402 201-480c0-43.333 9.5-84.333 28.5-123s45.5-71.333 79.5-98c13.333 48 32.167 93 56.5 135S419-486 453-452z" fill="#0c80d6"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+8 -6
View File
@@ -16,6 +16,7 @@
<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" />
@@ -28,14 +29,15 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- Polyfill Intl.Segmenter for Firefox 124 and earlier. This can be safely removed in the end of 2024. -->
<script crossorigin="anonymous" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></script>
<!-- ReactJS -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.development.js"></script>
<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@5/react-intl.iife.js"></script>
<!-- Firebase for push notifications -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9/firebase-messaging.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@7/react-intl.iife.js"></script>
<!-- Displaying QR code with ID of the topic -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.js"></script>
</head>
<body>
+7 -6
View File
@@ -5,7 +5,7 @@
<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 messaging in a browser" />
<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" />
@@ -13,6 +13,7 @@
<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" />
@@ -22,11 +23,11 @@
<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@17.0.2/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.20.7/react-intl.iife.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9.4.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9.4.0/firebase-messaging.js"></script>
<script crossorigin="anonymous" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@7.1.6/react-intl.iife.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+6 -1
View File
@@ -36,5 +36,10 @@
"platform": "itunes",
"url": "https://apps.apple.com/us/app/tinode/id1483763538"
}
]
],
"permissions": {
"audio-capture": {
"description": "Required for recording voice messages"
}
}
}
+6638 -8643
View File
File diff suppressed because it is too large Load Diff
+39 -33
View File
@@ -1,13 +1,15 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.18.0-beta1",
"version": "0.25.0-beta1",
"repository": {
"type": "git",
"url": "https://github.com/tinode/example-react-js.git"
"url": "git+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",
@@ -16,17 +18,14 @@
"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"
"compile": "formatjs compile",
"test": "jest"
},
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
"browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead",
"files": [
"umd/index.prod.js",
"umd/index.prod.js.map",
"umd/index.dev.js",
"umd/index.dev.js.map",
"umd/tinode.prod.js",
"umd/tinode.dev.js",
"audio/msg.mp3",
"umd/*.js",
"umd/*.map",
"audio/*.m4a",
"css/base.css",
"css/base.min.css",
"img/*.png",
@@ -36,7 +35,6 @@
"index.html",
"manifest.json",
"service-worker.js",
"firebase-init.js",
"version.js"
],
"keywords": [
@@ -48,45 +46,53 @@
"web"
],
"locales": [
"ar",
"de",
"en",
"es",
"fr",
"it",
"ko",
"ro",
"ru",
"th",
"uk",
"zh",
"zh-TW"
],
"main": "umd/index.prod.js",
"bugs": {
"url": "https://github.com/tinode/example-react-js/issues"
"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.4.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-intl": "^5.20.2",
"tinode-sdk": "^0.18.0-beta1"
"firebase": "^11.0.0",
"libphonenumber-js": "^1.10.40",
"qrcodejs": "^1.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-intl": "^7.0.0",
"tinode-sdk": "^0.25.0-beta1",
"webm-duration-fix": "^1.0.4"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@formatjs/cli": "^4.2.20",
"babel-loader": "^8.2.2",
"babel-plugin-formatjs": "^10.2.19",
"browserslist": "^4.16.5",
"copy-webpack-plugin": "^9.0.0",
"cssnano": "^5.0.6",
"glob": "^7.1.7",
"mkdirp": "^1.0.4",
"postcss": "^8.3.2",
"postcss-cli": "^9.0.2",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2"
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@formatjs/cli": "^6.1.3",
"babel-loader": "^10.0.0",
"babel-plugin-formatjs": "^10.5.3",
"browserslist": "^4.21.10",
"copy-webpack-plugin": "^13.0.0",
"cssnano": "^7.0.6",
"glob": "^11.0.0",
"jest": "^30.0.0",
"postcss": "^8.4.27",
"postcss-cli": "^11.0.0",
"webpack": "^5.88.2",
"webpack-cli": "^6.0.0"
}
}
+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).
+8 -8
View File
@@ -5,19 +5,17 @@
const fs = require('fs');
const path = require('path');
const globSync = require('glob').sync;
const mkdirpSync = require('mkdirp').sync;
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
const BASE_LANG = 'en';
const LANG_DIR = './src/i18n/';
const LANG_PATTERN = './src/i18n/*.json';
const LANG_PATTERN_ONE = './src/i18n/%s.json';
const FINAL_FILE = './src/messages.json';
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
const args = process.argv.slice(2);
function getLocaleFromFileName(filename) {
const parts = filename.split('/');
const parts = filename.split(path.sep);
return parts[parts.length - 1].split('.json')[0];
}
@@ -99,8 +97,9 @@ globSync(LANG_PATTERN)
fs.writeFileSync(filename, JSON.stringify(translated, null, 2)+"\n");
});
// Combine all translations into a single json {"lang_1": {...}, "lang_2: {...}"}
// 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)
@@ -122,6 +121,7 @@ const messages = globSync(LANG_PATTERN)
return { ...collection, ...descriptors };
}, {});
// Write a file with all translations combined.
mkdirpSync(path.dirname(FINAL_FILE));
fs.writeFileSync(FINAL_FILE, JSON.stringify(messages));
// Write minified translation files, one per language.
for (let lang in messages) {
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
}
+83 -51
View File
@@ -1,6 +1,6 @@
// Must be located at the root.
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.9.1/firebase-app.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.9.1/firebase-messaging.js');
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');
@@ -18,10 +18,18 @@ const i18n = {
'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",
},
'it': {
'new_message': "Nuovo Messaggio",
'new_chat': "Nuova Chat",
},
'ko': {
'new_message': "새로운 메시지",
'new_chat': "새로운 채팅",
@@ -34,6 +42,14 @@ const i18n = {
'new_message': "Новое сообщение",
'new_chat': "Новый чат",
},
'th': {
'new_message': "ข้อความใหม่",
'new_chat': "แชทใหม่",
},
'uk': {
'new_message': "Нове повідомлення",
'new_chat': "Новий чат",
},
'zh': {
'new_message': "新讯息",
'new_chat': "新聊天",
@@ -41,9 +57,17 @@ const i18n = {
'zh-TW': {
'new_message': "新訊息",
'new_chat': "新聊天",
},
'ar': {
'new_message': "رسالة جديدة",
'new_chat': "محادثة جديدة",
}
};
self.i18nMessage = function(id) {
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
@@ -55,76 +79,84 @@ firebase.initializeApp(FIREBASE_INIT);
const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage((payload) => {
if (payload.data.silent == 'true') {
// TODO: if this is an 'msg', fetch the new message in the background.
return;
}
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(pushType == 'msg' ? 'new_message' : 'new_chat');
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);
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', event => {
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 notification = event.notification;
notification.close();
const data = event.notification.data;
event.notification.close();
if (!data) {
console.info("Missing 'data' in notification", event.notification);
return;
}
const urlHash = '#/' + notification.data.topic;
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(notification.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];
})
.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);
// 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 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);
}));
return clients.openWindow(url);
}));
});
// This is needed for 'Add to Home Screen'.
@@ -133,13 +165,13 @@ self.addEventListener('fetch', event => {
return;
}
event.respondWith((async () => {
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)});
const cachedResponse = await cache.match(event.request, { ignoreSearch: (self.location.origin == reqUrl.origin) });
if (cachedResponse) {
return cachedResponse;
}
+59 -8
View File
@@ -1,7 +1,7 @@
import { PACKAGE_VERSION } from './version.js';
// Name of this application, used in the User-Agent.
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.17');
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.24');
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
@@ -16,11 +16,9 @@ export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
export const LOGGING_ENABLED = true;
// Minimum time between two keypress notifications, milliseconds.
export const KEYPRESS_DELAY = 3*1000;
// Delay before sending a {note} for reciving a message, milliseconds.
export const RECEIVED_DELAY = 500;
export const KEYPRESS_DELAY = 3_000;
// Delay before sending a read notification, milliseconds.
export const READ_DELAY = 1000;
export const READ_DELAY = 1_500;
// The default shortest allowed tag length. Matches the value on the server.
export const MIN_TAG_LENGTH = 2;
@@ -55,6 +53,9 @@ 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;
@@ -64,7 +65,7 @@ export const MESSAGES_PAGE = 24;
// 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 = 262144;
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.
@@ -78,8 +79,13 @@ 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"
@@ -89,6 +95,8 @@ export const MAX_ONLINE_IN_TOPIC = 4;
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 = 24;
// Length of message previews in chat list, in characters.
export const MESSAGE_PREVIEW_LENGTH = 80;
@@ -96,13 +104,56 @@ 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 = 96;
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 = 'email:support@tinode.co';
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';
// When user starts a P2P topic, automatic subscription does not
// happen until the first message. Setting this option to true enables
// immediate subscription.
export const IMMEDIATE_P2P_SUBSCRIPTION = false;
// URL schemes permitted to appear in links (i.e. no javascript: scheme).
export const CLICKABLE_URL_SCHEMES = ['http', 'https', 'ftp', 'ftps'];
// Size of the QR code in the app.
export const QRCODE_SIZE = 128;
// How much time has to pass to consider computer awoken from sleep (80 sec).
// Chrome throttles ticker events in inactive tabs to once per minute. This
// value ensures that these once-a-minute events do not trigger a reconnect.
export const WAKE_UP_TIMEOUT = 80000;
// Timer interval between checks if computer woke up from sleep (1 sec).
export const WAKE_UP_TICK = 1000;
// Swipe distance to register as a gesture, in pixels.
export const MIN_SWIPE_DISTANCE = REM_SIZE * 3;
// Self-topic avatar as an SVG icon encoded as data URI
export const SELF_AVATAR_URI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZpZXdCb3g9IjAgMCAxNyAxNyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMy41NjIgMXYxNS40NTlsNC42ODYtMy4yNyA0Ljc1MiAzLjI2di0xNS40NDloLTkuNDM4ek0xMiAxNC41NTFsLTMuNzU2LTIuNTc4LTMuNjgxIDIuNTY4di0xMi41NDFoNy40Mzd2MTIuNTUxeiIgZmlsbD0iIzU1NTU1NSIgLz48L3N2Zz4=' // eslint-disable-line max-len
// Toast display duration, milliseconds.
export const TOAST_DURATION = 3_000;
// Initial/default color scheme.
export const DEFAULT_COLOR_SCHEME = 'light dark';
// Default text size in points.
export const DEFAULT_TEXT_SIZE = 10;
// MIME type for the legacy Drafty form-response data.
export const DRAFTY_FR_MIME_TYPE_LEGACY = 'application/json'; // Remove in 2026.
+9
View File
@@ -0,0 +1,9 @@
// 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
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
+1868
View File
File diff suppressed because it is too large Load Diff
+543 -32
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "Hinzufügen",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "Mitglieder hinzufügen",
"defaultMessage": "Add members",
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "Inhalt gelöscht",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "herunterladen",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "Dateianhang",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "Formular:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "Bild",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "Geben Sie den Bestätigungscode ein, der per {method} geschickt wurde:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "Passwort per E-Mail wiederherstellen:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "Kontakte:",
"defaultMessage": "Contacts:",
"translation": "Kontakte",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "Zuletzt gesehen",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "Liste: email:lisa@beispiel.de, tel:17025550003...",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1229,13 +1215,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "bestätigen",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "Beschreibung (optional)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "Eine E-Mail wurde an {email} gesendet. Folgen Sie den Anweisungen in der E-Mail, um Ihr Passwort zurückzusetzen.",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "Eine E-Mail mit Sicherheitscode wurde gesendet.",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "Neues Kennwort",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "Nicht unterstützt",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "Eingehender Anruf",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "Ausgehender Anruf",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "Du",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "Videoanruf",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "Du redest schon!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "abgebrochen",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "verpasster",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "abgelehnt",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "getrennt",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Verwenden Sie eine sichere Verbindung",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Erfolgreich bestätigt",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Bearbeiten",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Bearbeitung",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", bearbeitet",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "Bild hinzufügen",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "Datei anhängen",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "Aufnahme löschen",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "Wiedergabe pausieren",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "Aufnahme abspielen",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "Sprachnachricht aufnehmen",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "Wiedergabe fortsetzen",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "Nachricht senden",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "Datei hierher ziehen",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "Videoaufnahme",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "in Arbeit",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "Anruf",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "Das Format dieses Videos wird nicht erkannt",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "Laden...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "Ich habe Code",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "SMS zum Zurücksetzen des Passworts senden",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "Mobiltelefonnummer",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "Eine SMS mit einem Sicherheitscode wurde gesendet.",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "Handynummer erforderlich",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "Aktuelle E-Mail",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "Aktuelle Telefonnummer",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "Neue E-Mail",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "Neue Telefonnummer",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "E-Mail ändern",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "Telefonnummer ändern",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "besetzt",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "Audioaufnahme konnte nicht initialisiert werden",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "Anpinnen",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "Lösen",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "Passwort erfolgreich zurückgesetzt",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "QR-Code scannen",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "Notizen, Nachrichten, Links, Dateien für die Nachwelt gespeichert",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "Gespeicherte Nachrichten",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "Nachricht nicht gefunden",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "Nachricht gelöscht",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "Alias (optional)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "Alias:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(bereits vergeben)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(ungültig)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "Alias",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "Land auswählen",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "In die Zwischenablage kopiert",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Dunkel",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Hell",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Systemstandard",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Thema:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Textgröße:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "Online-Status und Tipp-Indikatoren verbergen.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "Senden mit {key}",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "Drücken Sie Enter für eine neue Zeile",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "Senden mit Enter",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "Drücken Sie Shift + Enter für eine neue Zeile",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "Allgemein",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "Tastatur:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "Anheften",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "Loslösen",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, one {{count, number} Abonnent} other {{count, number} Abonnenten}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "Mitglieder:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "Abonnenten:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, one {{count, number} Mitglied} other {{count, number} Mitglieder}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+544 -89
View File
@@ -34,13 +34,6 @@
"missing": false,
"obsolete": false
},
"action_more": {
"translation": "More",
"defaultMessage": "More",
"description": "Action for showing more content",
"missing": false,
"obsolete": true
},
"action_report_chat": {
"translation": "Report Conversation",
"defaultMessage": "Report Conversation",
@@ -104,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "Add another",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "Add members",
"defaultMessage": "Add members",
@@ -314,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "content deleted",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "download",
"defaultMessage": "download",
@@ -331,21 +310,21 @@
"drafty_attachment": {
"translation": "Attachment",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "Form:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "Picture",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -371,8 +350,8 @@
"obsolete": false
},
"enter_confirmation_code_prompt": {
"translation": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"translation": "Confirmation code",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -461,13 +440,6 @@
"missing": false,
"obsolete": false
},
"label_default_access": {
"translation": "Default access mode:",
"defaultMessage": "Default access mode:",
"description": "Section title",
"missing": false,
"obsolete": true
},
"label_default_access_mode": {
"translation": "Default access mode:",
"defaultMessage": "Default access mode:",
@@ -554,7 +526,7 @@
},
"label_reset_password": {
"translation": "Send a password reset email:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -600,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "Contacts:",
"defaultMessage": "Contacts:",
"translation": "Contacts",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -643,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "Last seen",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1028,18 +1000,11 @@
},
"search_placeholder": {
"translation": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Sidepanel title for TopicCommonView.",
"missing": false,
"obsolete": true
},
"sidepanel_title_acc_notifications": {
"translation": "Notifications",
"defaultMessage": "Notifications",
@@ -1047,13 +1012,6 @@
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_security": {
"translation": "Security",
"defaultMessage": "Security",
"description": "Sidepanel title for AccSecurityView.",
"missing": false,
"obsolete": true
},
"sidepanel_title_acc_support": {
"translation": "Support",
"defaultMessage": "Support",
@@ -1089,13 +1047,6 @@
"missing": false,
"obsolete": false
},
"sidepanel_title_crop": {
"translation": "Drag to Adjust",
"defaultMessage": "Drag to Adjust",
"description": "Sidepanel title for AvatarCropView.",
"missing": false,
"obsolete": true
},
"sidepanel_title_login": {
"translation": "Sign In",
"defaultMessage": "Sign In",
@@ -1187,13 +1138,6 @@
"missing": false,
"obsolete": false
},
"title_info": {
"translation": "Info",
"defaultMessage": "Info",
"description": "Title for InfoView",
"missing": false,
"obsolete": true
},
"title_manage_tags": {
"translation": "Manage",
"defaultMessage": "Manage",
@@ -1271,13 +1215,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "confirm",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "Optional description",
"defaultMessage": "Description (optional)",
@@ -1299,20 +1236,6 @@
"missing": false,
"obsolete": false
},
"panel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Title for TopicCommonView.",
"missing": false,
"obsolete": true
},
"panel_title_acc_security": {
"translation": "Security",
"defaultMessage": "Security",
"description": "Title for TopicSecirityView and AccSecurityView.",
"missing": false,
"obsolete": true
},
"panel_title_crop": {
"translation": "Drag to Adjust",
"defaultMessage": "Drag to Adjust",
@@ -1377,8 +1300,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "An email with security code has been sent.",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1409,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "New password",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "Unsupported",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "Incoming call",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "Outgoing call",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "You",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "Video call",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "You already in an ongoing call!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "cancelled",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "missed",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "declined",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "disconnected",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Use secure connection",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmed successfully",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Edit",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Editing",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", edited",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "Add image",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "Attach file",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "Delete",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "Pause",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "Play",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "Record voice message",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "Resume",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "Send message",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "Drag file here",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "Video recording",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "in progress",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "Call",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "Format of this video is not recognized",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "Loading...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "I have code",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "Send a password reset SMS:",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "Mobile phone number",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "A text message with security code has been sent.",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "Mobile phone number required",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "Current email",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "Current phone number",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "New email",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "New phone number",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "Change email",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "Change phone number",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "busy",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "Failed to initialize audio recording",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "Pin",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "Unpin",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "Password reset successfully",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "Scan QR Code",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "Notes, messages, links, files saved for posterity",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "Saved messages",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "message not found",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "message deleted",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "Alias (optional)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "Alias:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(already taken)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(invalid)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "Alias",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "Select country",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "Copied to clipboard",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Dark",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Light",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "System default",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Theme:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Text size:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "Hide your online status and typing indicators.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "Send on {key}",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "Press Enter for new line",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "Send on Enter",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "Press Shift + Enter for new line",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "Keyboard:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "Pin",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "Unpin",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, one {{count, number} subscriber} other {{count, number} subscribers}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "Members:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "Subscribers:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, one {{count, number} member} other {{count, number} members}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+543 -32
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "Añadir contacto",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "Añadir miembros",
"defaultMessage": "Add members",
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "este mensaje fue eliminado",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "descargar",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "Archivo",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "Formulario:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "Imagen",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "Introduzca el código de confirmación enviado a tu {method}:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "Enviar un correo electrónico de restablecimiento de contraseña:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "Contactos:",
"defaultMessage": "Contacts:",
"translation": "Contactos",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "Últ. vez",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "Ej. email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1222,13 +1208,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "confirmar",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "Descripción (opcional)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "Se ha enviado un correo electrónico a {email}. Siga las instrucciones del correo electrónico para restablecer su contraseña.",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "Se ha enviado un correo electrónico con el código de seguridad.",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "Nueva contraseña",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "No soportado",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "Llamada entrante",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "Llamada saliente",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "Tú",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "Videollamada",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "¡Ya estás hablando!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "cancelada",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "perdida",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "rechazada",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "desconectada",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Usar conexión segura",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmado con éxito",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Editar",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Edición",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", editado",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "Añadir imagen",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "Adjuntar archivo",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "Eliminar grabación",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "Pausar reproducción",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "Reproducir grabación",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "Grabar mensaje de voz",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "Reanudar la reproducción",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "Enviar mensaje",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "Arrastra el archivo aquí",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "Grabación de vídeo",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "en progreso",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "Llamada",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "No se reconoce el formato de este video",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "Cargando...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "Tengo código",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "Enviar SMS para restablecer la contraseña",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "Número de teléfono móvil",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "Se ha enviado un mensaje de texto con un código de seguridad.",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "Número de teléfono móvil requerido",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "Correo electrónico actual",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "Número de teléfono actual",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "Nuevo correo electrónico",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "Nuevo número de teléfono",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "Cambiar correo electrónico",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "Cambiar número de teléfono",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "ocupado",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "Fijar",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "Desanclar",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "Error al inicializar la grabación de audio",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "Restablecimiento de contraseña con éxito",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "Escanear código QR",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "Notas, mensajes, enlaces, archivos guardados para la posteridad",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "Mensajes guardados",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "Mensaje no encontrado",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "Mensaje eliminado",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "Alias (opcional)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "Alias:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(ya tomado)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(inválido)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "Alias",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "Seleccionar país",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "Copiado al portapapeles",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Oscuro",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Claro",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Predeterminado del sistema",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Tema:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Tamaño del texto:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "Ocultar tu estado en línea e indicadores de escritura.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "Enviar con {key}",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "Presiona Enter para nueva línea",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "Enviar con Enter",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "Presiona Shift + Enter para nueva línea",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "Teclado:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "Fijar",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "Desfijar",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, one {{count, number} suscriptor} other {{count, number} suscriptores}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "Miembros:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "Suscriptores:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, one {{count, number} miembro} other {{count, number} miembros}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+313 -21
View File
@@ -23,6 +23,22 @@
"defaultMessage": "Report Conversation",
"description": "Flat button [Report Group]"
},
"alias_already_taken": {
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken"
},
"alias_editing_placeholder": {
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias"
},
"alias_invalid": {
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias"
},
"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"
@@ -55,10 +71,6 @@
"defaultMessage": "Blocked contacts ({count})",
"description": "Blocked contacts link"
},
"button_add_another": {
"defaultMessage": "Add another",
"description": "Call to action [+ add another]"
},
"button_add_members": {
"defaultMessage": "Add members",
"description": "Flat button [Add members] (to topic)"
@@ -119,10 +131,54 @@
"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"
@@ -155,6 +211,18 @@
"defaultMessage": "Code does not match",
"description": "Error message when the credential validation code is incorrect."
},
"color_schema_dark": {
"defaultMessage": "Dark",
"description": "Name of the color schema"
},
"color_schema_light": {
"defaultMessage": "Light",
"description": "Name of the color schema"
},
"color_schema_system": {
"defaultMessage": "System default",
"description": "Name of the color schema"
},
"contacts_not_found": {
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found"
@@ -163,10 +231,22 @@
"defaultMessage": "No contacts match ''{query}''",
"description": "Shown in ContactsView when search returned no results"
},
"cred_confirmed_successfully": {
"defaultMessage": "Confirmed successfully",
"description": "Notification message 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"
@@ -179,10 +259,6 @@
"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."
},
"deleted_content": {
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted"
},
"description_editing_placeholder": {
"defaultMessage": "Description (optional)",
"description": "Placeholder for editing topic description"
@@ -193,15 +269,31 @@
},
"drafty_attachment": {
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview"
"description": "Comment for attachment in Drafty"
},
"drafty_form": {
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview"
"description": "Comment for form in Drafty"
},
"drafty_image": {
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview"
"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",
@@ -216,13 +308,17 @@
"description": "Call to action to enable peer's messaging"
},
"enter_confirmation_code_prompt": {
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code"
},
"error_invalid_id": {
"defaultMessage": "Invalid ID",
"description": "Error message"
},
"failed_to_init_audio": {
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available"
},
"file_attachment_too_large": {
"defaultMessage": "The file size {size} exceeds the {limit} limit.",
"description": "Error message when attachment is too large"
@@ -254,10 +350,46 @@
"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"
},
"incognito_mode_description": {
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle"
},
"invalid_content": {
"defaultMessage": "invalid content",
"description": "Shown when the message is unreadable"
@@ -266,10 +398,22 @@
"defaultMessage": "Invalid security token",
"description": "Error message when resetting password."
},
"label_alias": {
"defaultMessage": "Alias:",
"description": "Label for user or topic alias"
},
"label_alias_edit": {
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias"
},
"label_client": {
"defaultMessage": "Client:",
"description": "Label for a client version"
},
"label_color_schema": {
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings"
},
"label_content_type": {
"defaultMessage": "Content type:",
"description": "Label for file content type (mime)"
@@ -294,6 +438,14 @@
"defaultMessage": "Incognito mode:",
"description": "Label for incognito mode toggle"
},
"label_keyboard": {
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings"
},
"label_member_count": {
"defaultMessage": "Members:",
"description": "Label for the count of group topic members"
},
"label_message_sound": {
"defaultMessage": "Message sound:",
"description": "Label for message sounds toggle"
@@ -302,6 +454,10 @@
"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"
@@ -327,9 +483,13 @@
"description": "Label for push notifications switch"
},
"label_reset_password": {
"defaultMessage": "Send a password reset email:",
"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:"
},
@@ -348,6 +508,14 @@
"defaultMessage": "Size:",
"description": "Label for file size"
},
"label_subscriber_count": {
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers"
},
"label_text_size": {
"defaultMessage": "Text size:",
"description": "Label adjusting text size"
},
"label_topic_name": {
"defaultMessage": "Name",
"description": "Label for editing topic name"
@@ -356,8 +524,12 @@
"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:",
"defaultMessage": "Contacts",
"description": "Label for user contacts"
},
"label_user_id": {
@@ -381,7 +553,7 @@
"description": "Label for current user permissions"
},
"last_seen_timestamp": {
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online"
},
"leave_chat_warning": {
@@ -400,14 +572,26 @@
"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"
},
"member_count": {
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members"
},
"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"
@@ -432,6 +616,10 @@
"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]"
@@ -472,6 +660,22 @@
"defaultMessage": "Unmute",
"description": "Turn notifications on"
},
"menu_item_video_call": {
"defaultMessage": "Video call",
"description": "Start video call"
},
"message_deleted": {
"defaultMessage": "message deleted",
"description": "Pinned message is deleted"
},
"message_edited_marker": {
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited"
},
"message_not_found": {
"defaultMessage": "message not found",
"description": "Pinned message is not found"
},
"message_sending": {
"defaultMessage": "sending...",
"description": "Message being sent, in place of time stamp"
@@ -488,10 +692,22 @@
"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"
@@ -500,6 +716,10 @@
"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."
@@ -536,14 +756,26 @@
"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 has been sent to {email}. Follow the directions in the email to reset your password.",
"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_reset_success": {
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset."
},
"password_unchanged_prompt": {
"defaultMessage": "Unchanged",
"description": "Message in editor while password is unchanged"
@@ -600,6 +832,14 @@
"defaultMessage": "phone",
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
},
"pin_chat": {
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top."
},
"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'"
@@ -632,6 +872,10 @@
"defaultMessage": "save",
"description": "Call to save an attachment"
},
"scan_qr_code": {
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code"
},
"search_for_contacts": {
"defaultMessage": "Use search to find contacts",
"description": "Text shown in contacts view when user entered no search query."
@@ -641,9 +885,41 @@
"description": "Text shown in contacts view when query returned no results."
},
"search_placeholder": {
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field"
},
"select_country": {
"defaultMessage": "Select country",
"description": "Placeholder for the country selector"
},
"self_topic_comment": {
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI"
},
"self_topic_name": {
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI"
},
"send_command": {
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter"
},
"send_command_explained": {
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled"
},
"send_plain": {
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter"
},
"send_plain_explained": {
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled"
},
"sidepanel_title_acc_general": {
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView."
},
"sidepanel_title_acc_notifications": {
"defaultMessage": "Notifications",
"description": "Sidepanel title for AccNotificationsView."
@@ -692,6 +968,10 @@
"defaultMessage": "Stay logged in",
"description": "Label for a checkbox"
},
"subscriber_count": {
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers"
},
"tabtitle_find_user": {
"defaultMessage": "find",
"description": "Tab title Find"
@@ -712,6 +992,10 @@
"defaultMessage": "No tags defined. Add some.",
"description": ""
},
"text_copied": {
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard"
},
"title_all_contacts": {
"defaultMessage": "All Contacts",
"description": "Section title [All Contacts]"
@@ -756,6 +1040,18 @@
"defaultMessage": "Unnamed",
"description": "Title shown when the topic has no name"
},
"unpin_chat": {
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat."
},
"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."
@@ -767,9 +1063,5 @@
"user_not_found": {
"defaultMessage": "Not found",
"description": "In place of a user's full name when the user is not found."
},
"validate_credential_action": {
"defaultMessage": "confirm",
"description": "Validate credentail call to action"
}
}
+1868
View File
File diff suppressed because it is too large Load Diff
+1868
View File
File diff suppressed because it is too large Load Diff
+543 -32
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "다른항목추가",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "회원추가",
"defaultMessage": "Add members",
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "내용이 삭제되었습니다",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "다운로드",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "부착",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "형태:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "이미지",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "{방법}으로보낸확인코드를입력하십시오:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "비밀번호재설정이메일보내기: ",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "연락처: ",
"defaultMessage": "Contacts:",
"translation": "연락처",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "마지막으로본",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "email:alice@example.com,tel:17025550003...와같은목록",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1222,13 +1208,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "확인",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "설명(선택 사항)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "이메일이 {email}(으)로 전송되었습니다. 이메일의 지침에 따라 비밀번호를 재설정하세요.",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "보안 코드가 포함된 이메일이 전송되었습니다.",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "새 비밀번호",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "지원되지 않음",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "수신 전화",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "발신 전화",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "너",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "영상 통화",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "당신은 이미 말하고 있습니다!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "취소 된",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "부재중",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "통화 거부",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "연결 끊김",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "보안 연결 사용",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "성공적으로 확인됨",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "편집",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "교정하다",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": " 수정됨",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "이미지 추가",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "파일을 첨부",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "녹음 삭제",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "재생 일시 중지",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "녹음 재생",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "음성 메시지 녹음",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "재생 재개",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "문자 보내",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "여기로 파일 드래그",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "녹화",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "통화 중",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "전화",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "이 동영상의 형식을 인식할 수 없습니다.",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "로드 중...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "코드가 있습니다",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "비밀번호 재설정을 위해 SMS 보내기",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "휴대전화 번호",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "보안 코드가 포함된 문자 메시지가 전송되었습니다.",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "휴대전화 번호 필요",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "현재 이메일",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "현재 전화번호",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "새 이메일 주소",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "새 전화번호",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "이메일 변경",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "전화번호 변경",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "바쁘다",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "고정",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "고정 해제",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "오디오 녹음 초기화 실패",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "비밀번호 재설정 성공",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "QR 코드 스캔",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "후세를 위해 저장된 메모, 메시지, 링크, 파일",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "저장된 메시지",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "메시지를 찾을 수 없음",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "메시지 삭제됨",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "별칭(선택 사항)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "별칭:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(이미 취함)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(잘못된)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "별칭",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "국가 선택",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "클립보드에 복사됨",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "어둡게",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "밝게",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "시스템 기본값",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "테마:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "텍스트 크기:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "온라인 상태와 입력 표시기를 숨깁니다.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "{key}로 전송",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "새 줄로 이동하려면 Enter를 누르세요",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "Enter로 전송",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "새 줄로 이동하려면 Shift + Enter를 누르세요",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "일반",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "키보드:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "고정",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "고정 해제",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, other {구독자 {count, number}명}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "멤버:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "구독자:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, other {멤버 {count, number}명}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+543 -32
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "Adaugă",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "Adaugă persoane",
"defaultMessage": "Add members",
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "conținut șters",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "download",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "Atasament",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "Formă:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "Imagine",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "Introu codul de confirmare trimis pe {method}:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "Resetează parola:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "Contacte:",
"defaultMessage": "Contacts:",
"translation": "Contacte",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "Văzut ultima dată",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "Caută după e-mail sau număr telefon...",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1222,13 +1208,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "confirmă",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "Descriere (opțional)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "Un e-mail a fost trimis către {email}. Urmați instrucțiunile din e-mail pentru a vă reseta parola.",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "A fost trimis un e-mail cu cod de securitate.",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "Parolă Nouă",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "Neacceptat",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "Apel primit",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "Apel efectuat",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "Tu",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "Apel video",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "Deja vorbesti!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "anulat",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "pierdut",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "refuzat",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "deconectat",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Utilizați conexiune securizată",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmat cu succes",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Editează",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Editare",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", editat",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "Adauga imagine",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "Ataseaza fisier",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "Ștergeți înregistrarea",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "Întrerupeți redarea",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "Redați înregistrarea",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "Înregistrați mesajul vocal",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "Reluați redarea",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "Trimite mesaj",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "Trageți fișierul aici",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "Înregistrare video",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "în curs",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "Apel",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "Formatul acestui videoclip nu este recunoscut",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "Se încarcă...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "Am cod",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "Trimite SMS pentru a reseta parola",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "Număr de telefon mobil",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "A fost trimis un mesaj text cu cod de securitate.",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "Este necesar un număr de telefon mobil",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "E-mail curent",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "Număr de telefon actual",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "E-mail nou",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "Număr de telefon nou",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "Schimbați adresa de e-mail",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "Schimbați numărul de telefon",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "ocupat",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "Fixați",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "Anulați fixarea",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "Inițializarea înregistrării audio a eșuat",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "Resetarea parolei cu succes",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "Scanați codul QR",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "Note, mesaje, linkuri, fișiere salvate pentru posteritate",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "Mesaje salvate",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "mesajul nu a fost găsit",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "mesaj șters",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "Alias (opțional)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "Alias:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(deja luat)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(nevalid)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "Alias",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "Selectați țara",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "Copiat în clipboard",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Întunecat",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Luminos",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Implicit sistem",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Temă:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Dimensiune text:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "Ascunde statusul tău online și indicatorii de tastare.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "Trimite cu {key}",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "Apasă Enter pentru linie nouă",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "Trimite cu Enter",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "Apasă Shift + Enter pentru linie nouă",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "Tastatură:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "Fixează",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "Anulează fixarea",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, one {{count, number} abonat} few {{count, number} abonați} other {{count, number} de abonați}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "Membri:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "Abonați:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, one {{count, number} membru} few {{count, number} membri} other {{count, number} de membri}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+545 -34
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "Добавить",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "Добавить",
"defaultMessage": "Add members",
@@ -140,7 +133,7 @@
"obsolete": false
},
"button_edit": {
"translation": "Редактировать",
"translation": "Изменить",
"defaultMessage": "Edit",
"description": "Call to action [Edit]",
"missing": false,
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "удалено",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "скачать",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "Аттачмент",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "Форма:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "Картинка",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "Код подтверждения, полученный по {method}:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "Отправить емейл для смены пароля:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "Конакты:",
"defaultMessage": "Contacts:",
"translation": "Конакты",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "Был активен",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "Список, напр. email:alice@example.com, tel:+17025550003...",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1125,7 +1111,7 @@
"obsolete": false
},
"tags_not_found": {
"translation": "Тегов нет. Добавьте",
"translation": "Тегов нет. Добавьте.",
"defaultMessage": "No tags defined. Add some.",
"description": "",
"missing": false,
@@ -1222,13 +1208,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "подтвердить",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "Описание (не обязательно)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "Сообщение было отправлено на адрес {email}. Следуйте инструкциям в сообщении, чтобы изменить пароль.",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "Сообщение было отправлено на указанный email.",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "Новый пароль",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "Не поддерживается",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "Входящий звонок",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "Исходящий звонок",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "Вы",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "Видеозвонок",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "Вы уже звоните кому-то!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "отменен",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "пропущен",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "отклонен",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "разъединен",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Безопасное соединение",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Подтверждено успешно",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Изменить",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Редактирование",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", изменено",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "Добавить изображение",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "Прикрепить файл",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "Удалить запись",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "Пауза воспроизведения",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "Воспроизвести запись",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "Записать голосовое сообщение",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "Продолжить",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "Отправить сообщение",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "Перетащите файл сюда",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "Видеозапись",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "в процессе",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "Позвонить",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "Неизвестный формат видео файла",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "Загружается...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "У меня есть код",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "Отправить SMS для сброса пароля",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "Номер мобильного телефона",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "SMS с защитным кодом отправлен.",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "Номер мобильного телефона",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "Текущий e-mail",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "Текущий номер телефона",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "Новый e-mail",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "Новый номер телефона",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "Изменить e-mail",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "Изменить номер телефона",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "занято",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "Закрепить",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "Открепить",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "Не удалось инициализировать аудиозапись",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "Пароль успешно изменен",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "Отсканируйте QR код",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "Заметки, сообщения, ссылки, файлы, сохраненные для потомков",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "Сохраненные сообщения",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "сообщение не найдено",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "сообщение удалено",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "Псевдоним (необязательно)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "Псевдоним:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(уже занято)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(некорректно)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "Псевдоним",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "Выберите страну",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "Скопировано в буфер обмена",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Тёмная",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Светлая",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Системная",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Тема:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Размер текста:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "Скрывать онлайн статус и индикаторы набора текста.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "Отправлять на {key}",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "Нажмать Enter для новой строки",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "Отправлять на Enter",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "Нажимать Shift + Enter для новой строки",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "Основные",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "Клавиатура:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "Закрепить",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "Открепить",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, one {{count, number} подписчик} few {{count, number} подписчика} many {{count, number} подписчиков} other {{count, number} подписчиков}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "Участники:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "Подписчики:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, one {{count, number} участник} few {{count, number} участника} many {{count, number} участников} other {{count, number} участников}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+1868
View File
File diff suppressed because it is too large Load Diff
+1868
View File
File diff suppressed because it is too large Load Diff
+544 -33
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "新增其他人",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "新增成員",
"defaultMessage": "Add members",
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "內容已被刪除",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "下載",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "附加檔案",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "表單:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "圖片",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "輸入透過 {method} 傳送給您的確認碼:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "傳送重設密碼郵件:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "聯絡人:",
"defaultMessage": "Contacts:",
"translation": "聯絡人",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -600,7 +586,7 @@
"obsolete": false
},
"label_wire_transport": {
"translation": "Wire transport:",
"translation": "線路傳輸:",
"defaultMessage": "Wire transport:",
"description": "Label for wire transport selection in SettingsView",
"missing": false,
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "最後上線",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "例如 email:alice@example.com, tel:17025550003... 的清單",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1222,13 +1208,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "確認",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "說明(可選)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "一封電子郵件已發送至 {email}。按照電子郵件中的說明重置密碼。",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "已發送一封帶有安全代碼的電子郵件。",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "新密碼",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "不支持",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "來電",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "撥出電話",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "你",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "視頻電話",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "你已經在說話了!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "取消",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "未接",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "被拒",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "斷開連接",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "使用安全連接",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "確認成功",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "編輯",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "編輯",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ",編輯",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "添加圖片",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "附加文件",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "刪除錄音",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "暫停播放",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "播放錄音",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "錄製語音消息",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "恢復播放",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "發信息",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "將文件拖到這裡",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "視頻錄製",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "通話中",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "稱呼",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "無法識別此視頻的格式",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "加載中...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "我有代碼",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "發送短信重置密碼",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "手機號碼",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "已發送帶有安全代碼的短信。",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "需要手機號",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "當前郵箱",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "當前電話號碼",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "新電子郵件地址",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "新電話號碼",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "更改郵箱",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "更改電話號碼",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "忙碌的",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "置頂消息",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "取消置頂消息",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "初始化錄音失敗",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "密碼重置成功",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "掃描二維碼",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "為後代所儲存的註解、訊息、連結、檔案",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "已儲存的訊息",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "未找到訊息",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "訊息已刪除",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "別名(可選)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "別名:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(已採取)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(無效)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "別名",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "選擇國家",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "已複製到剪貼簿",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "深色",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "淺色",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "系統預設",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "主題:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "文字大小:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "隱藏您的線上狀態和正在輸入指示器。",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "按 {key} 傳送",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "按 Enter 換行",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "按 Enter 傳送",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "按 Shift + Enter 換行",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "一般",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "鍵盤:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "置頂",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "取消置頂",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, other {{count, number} 位訂閱者}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "成員:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "訂閱者:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, other {{count, number} 位成員}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+544 -33
View File
@@ -97,13 +97,6 @@
"missing": false,
"obsolete": false
},
"button_add_another": {
"translation": "加上另一个",
"defaultMessage": "Add another",
"description": "Call to action [+ add another]",
"missing": false,
"obsolete": false
},
"button_add_members": {
"translation": "添加成员",
"defaultMessage": "Add members",
@@ -307,13 +300,6 @@
"missing": false,
"obsolete": false
},
"deleted_content": {
"translation": "内容已删除",
"defaultMessage": "content deleted",
"description": "Shown when messages are deleted",
"missing": false,
"obsolete": false
},
"download_action": {
"translation": "下载",
"defaultMessage": "download",
@@ -324,21 +310,21 @@
"drafty_attachment": {
"translation": "附件",
"defaultMessage": "Attachment",
"description": "Comment for attachment in drafty preview",
"description": "Comment for attachment in Drafty",
"missing": false,
"obsolete": false
},
"drafty_form": {
"translation": "形式:",
"defaultMessage": "Form:",
"description": "Comment for form in drafty preview",
"description": "Comment for form in Drafty",
"missing": false,
"obsolete": false
},
"drafty_image": {
"translation": "图像",
"defaultMessage": "Picture",
"description": "Comment for embedded images in drafty preview",
"description": "Comment for embedded images in Drafty",
"missing": false,
"obsolete": false
},
@@ -365,7 +351,7 @@
},
"enter_confirmation_code_prompt": {
"translation": "输入通过{method}发送的验证码:",
"defaultMessage": "Enter confirmation code sent to you by {method}:",
"defaultMessage": "Confirmation code",
"description": "Request to enter confirmation code",
"missing": false,
"obsolete": false
@@ -455,7 +441,7 @@
"obsolete": false
},
"label_default_access_mode": {
"translation": "蓦然访问模式:",
"translation": "默认访问模式:",
"defaultMessage": "Default access mode:",
"description": "Label for default access mode",
"missing": false,
@@ -540,7 +526,7 @@
},
"label_reset_password": {
"translation": "发送密码重置邮件:",
"defaultMessage": "Send a password reset email:",
"defaultMessage": "Send a password reset email",
"description": "Label for password reset field",
"missing": false,
"obsolete": false
@@ -586,8 +572,8 @@
"obsolete": false
},
"label_user_contacts": {
"translation": "往来:",
"defaultMessage": "Contacts:",
"translation": "往来",
"defaultMessage": "Contacts",
"description": "Label for user contacts",
"missing": false,
"obsolete": false
@@ -629,7 +615,7 @@
},
"last_seen_timestamp": {
"translation": "最后可见",
"defaultMessage": "Last seen",
"defaultMessage": "Last seen: {timestamp}",
"description": "Label for the timestamp of when the user or topic was last online",
"missing": false,
"obsolete": false
@@ -1007,7 +993,7 @@
},
"search_placeholder": {
"translation": "列表如 email:alice@example.com, tel:+17025550003...",
"defaultMessage": "List like email:alice@example.com, tel:17025550003...",
"defaultMessage": "List like alice@example.com, +17025550003...",
"description": "Placeholder in contacts search field",
"missing": false,
"obsolete": false
@@ -1222,13 +1208,6 @@
"missing": false,
"obsolete": false
},
"validate_credential_action": {
"translation": "确认",
"defaultMessage": "confirm",
"description": "Validate credentail call to action",
"missing": false,
"obsolete": false
},
"description_editing_placeholder": {
"translation": "说明(可选)",
"defaultMessage": "Description (optional)",
@@ -1314,8 +1293,8 @@
"obsolete": false
},
"password_reset_email_sent": {
"translation": "一封电子邮件已发送至 {email}。按照电子邮件中的说明重置密码。",
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
"translation": "已发送一封带有安全代码的电子邮件。",
"defaultMessage": "An email with security code has been sent.",
"description": "Notification that the email with password reset instructions has been sent",
"missing": false,
"obsolete": false
@@ -1353,5 +1332,537 @@
"description": "Contact search prompt when forwarding a message.",
"missing": false,
"obsolete": false
},
"label_new_password": {
"translation": "新密码",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
},
"drafty_unknown": {
"translation": "不支持",
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty",
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "来电",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "拨出电话",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "你",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "视频电话",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "你已经在说话了!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "取消",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "未接",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "被拒",
"defaultMessage": "declined",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "断开连接",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "使用安全连接",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "确认成功",
"defaultMessage": "Confirmed successfully",
"description": "Notification message that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "编辑",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "编辑",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ",编辑",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
},
"icon_title_add_image": {
"translation": "添加图片",
"defaultMessage": "Add image",
"description": "Icon tool tip for attaching an image",
"missing": false,
"obsolete": false
},
"icon_title_attach_file": {
"translation": "附加文件",
"defaultMessage": "Attach file",
"description": "Icon tool tip for attaching a file",
"missing": false,
"obsolete": false
},
"icon_title_delete": {
"translation": "删除录音",
"defaultMessage": "Delete recording",
"description": "Icon tool tip for deleting recorded audio",
"missing": false,
"obsolete": false
},
"icon_title_pause": {
"translation": "暂停播放",
"defaultMessage": "Pause playback",
"description": "Icon tool tip for pausing audio playback",
"missing": false,
"obsolete": false
},
"icon_title_play": {
"translation": "播放录音",
"defaultMessage": "Play recording",
"description": "Icon tool tip for starting audio playback",
"missing": false,
"obsolete": false
},
"icon_title_record_voice": {
"translation": "录制语音信息",
"defaultMessage": "Record voice message",
"description": "Icon tool tip for recording a voice message",
"missing": false,
"obsolete": false
},
"icon_title_resume": {
"translation": "恢复播放",
"defaultMessage": "Resume playback",
"description": "Icon tool tip for resuming audio playback",
"missing": false,
"obsolete": false
},
"icon_title_send": {
"translation": "发信息",
"defaultMessage": "Send message",
"description": "Icon tool tip for sending a message",
"missing": false,
"obsolete": false
},
"drag_file": {
"translation": "将文件拖到这里",
"defaultMessage": "Drag file here",
"description": "Prompt on the file drag-n-drop overlay banner",
"missing": false,
"obsolete": false
},
"drafty_video": {
"translation": "视频录制",
"defaultMessage": "Video recording",
"description": "Comment for videos embedded in Drafty",
"missing": false,
"obsolete": false
},
"call_in_progress": {
"translation": "通话中",
"defaultMessage": "in progress",
"description": "Label for call in progress",
"missing": false,
"obsolete": false
},
"menu_item_audio_call": {
"translation": "称呼",
"defaultMessage": "Call",
"description": "Start audio call",
"missing": false,
"obsolete": false
},
"unrecognized_video_format": {
"translation": "无法识别此视频的格式",
"defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid",
"missing": false,
"obsolete": false
},
"loading_note": {
"translation": "加载中...",
"defaultMessage": "Loading...",
"description": "Message shown when component is loading",
"missing": false,
"obsolete": false
},
"password_i_have_code": {
"translation": "我有代码",
"defaultMessage": "I have code",
"description": "Call to open field to enter password reset code",
"missing": false,
"obsolete": false
},
"label_reset_password_tel": {
"translation": "发送短信重置密码",
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone",
"missing": false,
"obsolete": false
},
"mobile_phone_number": {
"translation": "手机号",
"defaultMessage": "Mobile phone number",
"description": "Prompt for entering a mobile phone number",
"missing": false,
"obsolete": false
},
"password_reset_sms_sent": {
"translation": "已发送带有安全代码的短信。",
"defaultMessage": "A text message with security code has been sent.",
"description": "Notification that the SMS with password reset instructions has been sent",
"missing": false,
"obsolete": false
},
"mobile_number_required": {
"translation": "需要手机号",
"defaultMessage": "Mobile phone number required",
"description": "Error message",
"missing": false,
"obsolete": false
},
"current_email": {
"translation": "当前邮箱",
"defaultMessage": "Current email",
"description": "Label for old email",
"missing": false,
"obsolete": false
},
"current_phone": {
"translation": "当前电话号码",
"defaultMessage": "Current phone number",
"description": "Label for old phone",
"missing": false,
"obsolete": false
},
"new_email": {
"translation": "新电子邮件地址",
"defaultMessage": "New email",
"description": "Prompt for entering a new email",
"missing": false,
"obsolete": false
},
"new_phone_number": {
"translation": "新电话号码",
"defaultMessage": "New phone number",
"description": "Prompt for entering a new telephone number",
"missing": false,
"obsolete": false
},
"change_email": {
"translation": "更改邮箱",
"defaultMessage": "Change email",
"description": "Prompt to change current email",
"missing": false,
"obsolete": false
},
"change_phone": {
"translation": "更改电话号码",
"defaultMessage": "Change phone number",
"description": "Prompt to change current phone number",
"missing": false,
"obsolete": false
},
"call_busy": {
"translation": "忙碌的",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "置顶消息",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "取消置顶消息",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
},
"failed_to_init_audio": {
"translation": "初始化录音失败",
"defaultMessage": "Failed to initialize audio recording",
"description": "Error message when audio is not available",
"missing": false,
"obsolete": false
},
"password_reset_success": {
"translation": "密码重置成功",
"defaultMessage": "Password reset successfully",
"description": "Notification message that the password was successfully reset.",
"missing": false,
"obsolete": false
},
"scan_qr_code": {
"translation": "扫描二维码",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
},
"self_topic_comment": {
"translation": "为后人保存的注释、消息、链接、文件",
"defaultMessage": "Notes, messages, links, files saved for posterity",
"description": "Comment for self topic for UI",
"missing": false,
"obsolete": false
},
"self_topic_name": {
"translation": "已保存的消息",
"defaultMessage": "Saved messages",
"description": "Name of self topic for UI",
"missing": false,
"obsolete": false
},
"message_not_found": {
"translation": "未找到消息",
"defaultMessage": "message not found",
"description": "Pinned message is not found",
"missing": false,
"obsolete": false
},
"message_deleted": {
"translation": "消息已删除",
"defaultMessage": "message deleted",
"description": "Pinned message is deleted",
"missing": false,
"obsolete": false
},
"alias_editing_placeholder": {
"translation": "别名(可选)",
"defaultMessage": "Alias (optional)",
"description": "Placeholder for editing user or topic alias",
"missing": false,
"obsolete": false
},
"label_alias": {
"translation": "别名:",
"defaultMessage": "Alias:",
"description": "Label for user or topic alias",
"missing": false,
"obsolete": false
},
"alias_already_taken": {
"translation": "(已占用)",
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken",
"missing": false,
"obsolete": false
},
"alias_invalid": {
"translation": "(无效)",
"defaultMessage": "(invalid)",
"description": "Error message for invalid alias",
"missing": false,
"obsolete": false
},
"label_alias_edit": {
"translation": "别名",
"defaultMessage": "Alias",
"description": "Label for editing user or topic alias",
"missing": false,
"obsolete": false
},
"select_country": {
"translation": "选择国家",
"defaultMessage": "Select country",
"description": "Placeholder for the country selector",
"missing": false,
"obsolete": false
},
"text_copied": {
"translation": "已复制到剪贴板",
"defaultMessage": "Copied to clipboard",
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "深色",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "浅色",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "系统默认",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "主题:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "文字大小:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "隐藏您的在线状态和正在输入指示器。",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
},
"send_command": {
"translation": "按 {key} 发送",
"defaultMessage": "Send on {key}",
"description": "Config option to send message on hitting CTRL(or Cmd)-Enter",
"missing": false,
"obsolete": false
},
"send_command_explained": {
"translation": "按 Enter 换行",
"defaultMessage": "Press Enter for new line",
"description": "Explanation how to enter newline when [Send on CTRL-Enter] is enabled",
"missing": false,
"obsolete": false
},
"send_plain": {
"translation": "按 Enter 发送",
"defaultMessage": "Send on Enter",
"description": "Config option to send message on hitting Enter",
"missing": false,
"obsolete": false
},
"send_plain_explained": {
"translation": "按 Shift + Enter 换行",
"defaultMessage": "Press Shift + Enter for new line",
"description": "Explanation how to enter newline when [Send on Enter] is enabled",
"missing": false,
"obsolete": false
},
"sidepanel_title_acc_general": {
"translation": "常规",
"defaultMessage": "General",
"description": "Sidepanel title for AccGeneralView.",
"missing": false,
"obsolete": false
},
"label_keyboard": {
"translation": "键盘:",
"defaultMessage": "Keyboard:",
"description": "Label for send on enter settings",
"missing": false,
"obsolete": false
},
"pin_chat": {
"translation": "置顶",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning chat to the top.",
"missing": false,
"obsolete": false
},
"unpin_chat": {
"translation": "取消置顶",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the chat.",
"missing": false,
"obsolete": false
},
"subscriber_count": {
"translation": "{count, plural, other {{count, number} 位订阅者}}",
"defaultMessage": "{count, plural, one {{count, number} subscriber} two {{count, number} subscribers} three {{count, number} subscribers} few {{count, number} subscribers} many {{count, number} subscribers} other {{count, number} subscribers}}",
"description": "Count of channel subscribers",
"missing": false,
"obsolete": false
},
"label_member_count": {
"translation": "成员:",
"defaultMessage": "Members:",
"description": "Label for the count of group topic members",
"missing": false,
"obsolete": false
},
"label_subscriber_count": {
"translation": "订阅者:",
"defaultMessage": "Subscribers:",
"description": "Label for the count of channel subscribers",
"missing": false,
"obsolete": false
},
"member_count": {
"translation": "{count, plural, other {{count, number} 位成员}}",
"defaultMessage": "{count, plural, one {{count, number} member} two {{count, number} members} three {{count, number} members} few {{count, number} members} many {{count, number} members} other {{count, number} members}}",
"description": "Count of group topic members",
"missing": false,
"obsolete": false
}
}
+49 -13
View File
@@ -2,14 +2,45 @@
// Used to generate umd/index.prod.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { IntlProvider } from 'react-intl';
import allMessages from './messages.json';
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 = {
'ar': _ => import('./i18n.min/ar.json'),
'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'),
'th': _ => import('./i18n.min/th.json'),
'uk': _ => import('./i18n.min/uk.json'),
'zh': _ => import('./i18n.min/zh.json'),
'zh-TW': _ => import('./i18n.min/zh-TW.json')
};
// List of languages that are written right-to-left.
const rtl = ['ar', 'fa', 'he', 'ur'];
// 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);
@@ -19,21 +50,26 @@ const language = (params && params.hl) ||
navigator.userLanguage ||
'en';
// Get the base language 'en' from a more specific 'en_GB' or 'en-US' as a partial fallback.
const baseLanguage = language.toLowerCase().split(/[-_]/)[0];
// 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 = allMessages[language] ? langauge : allMessages[baseLanguage] ? baseLanguage : 'en';
const messages = allMessages[htmlLang];
const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
const html = document.getElementsByTagName('html')[0];
// Set lang attribute of the HTML element: <html lang="XX">
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
ReactDOM.render(
<React.StrictMode>
html.setAttribute('lang', htmlLang);
if (rtl.includes(baseLanguage)) {
// Set right-to-left dir attribute of the HTML element: <html dir="rtl">
html.setAttribute('dir', 'rtl');
}
// 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>
</React.StrictMode>,
document.getElementById('mountPoint')
);
));
+35 -11
View File
@@ -1,6 +1,4 @@
// File and image helper functions.
import { MAX_INBAND_ATTACHMENT_SIZE, MAX_IMAGE_DIM } from '../config.js';
import { bytesToHumanSize } from './strformat.js'
// Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
@@ -9,11 +7,13 @@ export const MIME_EXTENSIONS = ['jpg', 'gif', 'png',
// 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 photo.ref;
}
return null;
}
@@ -138,10 +138,10 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onerror = (err) => {
img.onerror = _ => {
reject(new Error("Image format unrecognized"));
};
img.onload = () => {
img.onload = _ => {
// Once the image is loaded, the URL is no longer needed.
URL.revokeObjectURL(img.src);
@@ -172,10 +172,10 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
export function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = (evt) => {
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = () => {
reader.onload = _ => {
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
};
reader.readAsDataURL(file);
@@ -186,10 +186,10 @@ export function fileToBase64(file) {
export function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = (evt) => {
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = () => {
reader.onload = _ => {
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
};
reader.readAsDataURL(blob);
@@ -199,7 +199,7 @@ export function blobToBase64(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 (!Array.isArray(items)) {
if (!items || !items.length) {
return false;
}
@@ -227,7 +227,7 @@ export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError)
// Get mime type from data URL header.
export function getMimeType(header) {
var mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
const mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
return (mime && mime.length > 1) ? mime[1] : null;
}
@@ -281,3 +281,27 @@ export function base64ToBlob(str, mime) {
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;
}
+322 -167
View File
@@ -3,33 +3,175 @@ 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 { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, 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, shortenFileName } from './strformat.js';
import { cancelablePromise, sanitizeImageUrl } from './utils.js';
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
const messages = defineMessages({
drafty_form: {
id: 'drafty_form',
defaultMessage: 'Form: ',
description: 'Comment for form in drafty preview'
description: 'Comment for form in Drafty'
},
drafty_attachment: {
id: 'drafty_attachment',
defaultMessage: 'Attachment',
description: 'Comment for attachment in drafty preview'
description: 'Comment for attachment in Drafty'
},
drafty_image: {
id: 'drafty_image',
defaultMessage: 'Picture',
description: 'Comment for embedded images in drafty preview'
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'
}
});
// Size the already scaled image.
// 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 'LN':
if (attr) {
// Ensure only safe URL schemes are clickable.
attr.href = typeof attr.href == 'string' ? sanitizeUrl(attr.href, CLICKABLE_URL_SCHEMES) : '';
}
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';
@@ -53,7 +195,7 @@ function handleImageData(el, data, attr) {
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) {
attr.src = this.authorizeURL(sanitizeImageUrl(attr.src));
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) {
@@ -63,137 +205,54 @@ function handleImageData(el, data, attr) {
}
attr.loading = 'lazy';
} else {
attr.src = 'img/broken_image.png';
attr.src = null;
}
} else {
// Use custom element instead of <img>.
// Use custom element instead of <img> or <video>.
el = UploadingImage;
}
return el;
}
function quotedImage(data) {
let promise;
// Get the blob from the image data.
if (data.val) {
const blob = base64ToBlob(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
// Additional processing of attached video 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 {
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
// Use custom element instead of <img> or <video>.
el = UploadingImage;
}
// Scale the blob.
return promise
.then(blob => {
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
}).then(scaled => {
data.mime = scaled.mime;
data.size = scaled.blob.size;
data.width = scaled.width;
data.height = scaled.height;
delete data.ref;
// Keeping the original file name, if provided: ex.data.name;
return blobToBase64(scaled.blob);
}).then(b64 => {
data.val = b64.bits;
return data;
}).catch(err => {
delete data.val;
delete data.name;
data.width = IMAGE_THUMBNAIL_DIM;
data.height = IMAGE_THUMBNAIL_DIM;
data.maxWidth = IMAGE_THUMBNAIL_DIM;
data.maxHeight = IMAGE_THUMBNAIL_DIM;
// Rethrow.
throw err;
});
return el;
}
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
// 'this' must contain:
// viewportWidth: this.props.viewportWidth;
// authorizeURL: this.props.tinode.authorizeURL
// onImagePreview: this.handleImagePreview
// onFormButtonClick: this.handleFormButtonClick
// onQuoteClick: this.handleQuoteClick (optional)
export function fullFormatter(style, data, values, key) {
if (!style) {
// Unformatted.
return values;
}
if (style == 'HD') {
// Hidden.
return null;
}
let el = Drafty.tagName(style);
const attr = Drafty.attrValue(style, data) || {};
attr.key = key;
switch (style) {
case 'HL':
// Highlighted text. Assign class name.
attr.className = 'highlight';
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;
default:
if (el == '_UNKN') {
// Unknown element.
el = React.Fragment;
values = [<i className="material-icons gray">extension</i>, ' '].concat(values || []);
}
break;
}
if (!el) {
return values;
}
return React.createElement(el, attr, values);
};
// Converts Drafty object into a one-line preview. 'this' is set by the caller.
// 'this' must contain:
// formatMessage: this.props.intl.formatMessage
@@ -204,14 +263,14 @@ export function previewFormatter(style, data, values, key) {
return values;
}
if (style == 'HD') {
// Hidden.
return null;
}
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;
@@ -229,7 +288,7 @@ export function previewFormatter(style, data, values, key) {
case 'IM':
// Replace image with '[icon] Image'.
el = React.Fragment;
values = [<i key="im" className="material-icons">photo</i>, this.formatMessage(messages.drafty_image)];
values = [<i key="im" className="material-icons">photo</i>, ' ', this.formatMessage(messages.drafty_image)];
break;
case 'BN':
el = 'span';
@@ -245,7 +304,7 @@ export function previewFormatter(style, data, values, key) {
break;
case 'EX':
if (data) {
if (data.mime == 'application/json') {
if (Drafty.isFormResponseType(data.mime)) {
// Ignore JSON attachments: they are form response payloads.
return null;
}
@@ -254,12 +313,32 @@ export function previewFormatter(style, data, values, key) {
delete data.ref;
}
el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>, this.formatMessage(messages.drafty_attachment)];
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 == '_UNKN') {
if (!el) {
// Unknown element.
el = React.Fragment;
values = [<i key="unkn" className="material-icons">extension</i>, ' '].concat(values || []);
values = [<i key="x0" className="material-icons gray">extension</i>, ' ', this.formatMessage(messages.drafty_unknown)];
}
break;
}
@@ -269,7 +348,7 @@ export function previewFormatter(style, data, values, key) {
return React.createElement(el, attr, values);
};
// Converts Drafty object into a quoted reply. 'this' is set by the caller.
// Converts Drafty object into a quoted reply; 'this' is set by the caller.
function inlineImageAttr(attr, data) {
attr.style = {
width: IMAGE_THUMBNAIL_DIM + 'px',
@@ -281,23 +360,48 @@ function inlineImageAttr(attr, data) {
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)
export function quoteFormatter(style, data, values, key) {
if (['BR', 'EX', 'IM', 'MN', 'QQ'].includes(style)) {
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];
@@ -305,6 +409,13 @@ export function quoteFormatter(style, data, values, key) {
// 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'
@@ -312,14 +423,10 @@ export function quoteFormatter(style, data, values, key) {
attr.className += ' ' + idToColorClass(data.val, false, true);
}
break;
case 'QQ':
attr.className = 'reply-quote';
attr.onClick = this.onQuoteClick;
break;
case 'EX':
let fname;
if (data) {
if (data.mime == 'application/json') {
if (Drafty.isFormResponseType(data.mime)) {
// Ignore JSON attachments: they are form response payloads.
return null;
}
@@ -339,42 +446,69 @@ export function quoteFormatter(style, data, values, key) {
}
// Create image thumbnail suitable for inclusion in a quote.
function quoteImage(data) {
function quoteImageOrVideo(data, isVideo) {
let promise;
// Get the blob from the image data.
if (data.val) {
const blob = base64ToBlob(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image"));
let bits, ref, mime;
if (isVideo) {
bits = data.preview;
mime = data.premime || 'image/jpeg';
ref = data.preref;
} else {
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
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 => {
// Cut the square from the center of the image and shrink it.
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
// 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 => {
data.mime = scaled.mime;
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 => {
data.val = b64.bits;
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;
@@ -384,12 +518,33 @@ function quoteImage(data) {
}
// Create a preview of a reply.
export function replyFormatter(style, data, values, key) {
if (style != 'IM') {
return quoteFormatter.call(this, style, data, values, key);
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);
}
const attr = inlineImageAttr.call(this, {key: key}, data);
attr.whenDone = cancelablePromise(quoteImage.call(this, data));
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
return React.createElement(React.Fragment, {key: key}, values);
return quoteFormatter.call(this, style, data, values, key);
}
+21 -18
View File
@@ -1,10 +1,10 @@
// Utility class for hash navigation.
// Parse hash as in http://www.example.com/path#hash as if it were
// path and arguments.
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]
// Split 'path/?args' into 'path/' and 'args', path -> parts[0], args->path[1].
const parts = hash.split('?', 2);
const params = {};
let path = [];
@@ -12,10 +12,11 @@ export default class HashNavigation {
path = parts[0].replace('#', '').split('/');
}
if (parts[1]) {
parts[1].split('&').forEach(function(part) {
const item = part.split('=');
if (item[0]) {
params[decodeURIComponent(item[0])] = decodeURIComponent(item[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));
}
});
}
@@ -31,7 +32,9 @@ export default class HashNavigation {
const args = [];
for (const key in params) {
if (params.hasOwnProperty(key)) {
args.push(key + '=' + params[key]);
if (params[key] !== undefined) {
args.push(key + '=' + encodeURIComponent(params[key]));
}
}
}
if (args.length > 0) {
@@ -41,38 +44,38 @@ export default class HashNavigation {
}
static addUrlParam(hash, key, value) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
parsed.params[key] = value;
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static removeUrlParam(hash, key) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
delete parsed.params[key];
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlSidePanel(hash, sidepanel) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[0] = sidepanel;
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlInfoPanel(hash, infopanel) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
if (infopanel) {
parsed.params.info = infopanel;
} else {
delete parsed.params.info;
}
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlTopic(hash, topic) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[1] = topic;
// Close InfoView on topic change.
delete parsed.params.info;
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
}
+57 -5
View File
@@ -16,12 +16,40 @@ export function shortDateFormat(then, locale) {
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
}
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
export function secondsToTime(seconds) {
const min = Math.floor(seconds / 60);
let sec = seconds % 60;
// 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;
return `${min}:${sec}`;
if (hours == 0) {
return `${min}:${sec}`;
}
return `${hours}:${min}:${sec}`;
}
// Convert a number of bytes to human-readable format.
@@ -71,3 +99,27 @@ export function letterTileColorId(userId) {
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);
}
// Truncate a string to a specific length and add ellipsis if needed.
// Support both LTR and RTL languages.
export function truncateString(str, maxLength) {
if (typeof str !== 'string') {
return str;
}
if (str.length <= maxLength) {
return str;
}
// Detect RTL characters in the string and truncate accordingly.
const isRTL = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(str);
return isRTL ? '…' + str.slice(0, maxLength - 1) : str.slice(0, maxLength - 1) + '…';
}
+189
View File
@@ -0,0 +1,189 @@
import {
shortDateFormat,
relativeDateFormat,
secondsToTime,
bytesToHumanSize,
shortenFileName,
idToColorClass,
flagEmoji,
truncateString
} from './strformat.js';
describe('shortDateFormat', () => {
jest
.useFakeTimers()
.setSystemTime(new Date('2025-07-25T12:30:00Z'));
test('should format for same day', () => {
const then = new Date('2025-07-25T10:15:00');
// Note: output depends on the test runner's locale and timezone.
// Assuming a locale where the format is HH:mm.
expect(shortDateFormat(then, 'en-US')).toMatch(/10:15/);
});
test('should format for same year, different day', () => {
const then = new Date('2025-06-20T10:15:00');
expect(shortDateFormat(then, 'en-US')).toMatch(/Jun 20, 10:15/);
});
test('should format for different year', () => {
const then = new Date('2024-07-25T10:15:00');
expect(shortDateFormat(then, 'en-US')).toBe('Jul 25, 2024');
});
});
describe('relativeDateFormat', () => {
// Mock current date to a fixed point for consistent testing.
jest
.useFakeTimers()
.setSystemTime(new Date('2025-07-25T12:30:00Z'));
test('should format for today', () => {
const then = new Date('2025-07-25T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('today');
});
test('should format for yesterday', () => {
const then = new Date('2025-07-24T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('yesterday');
});
test('should format for a past date', () => {
const then = new Date('2025-07-20T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('7/20/2025');
});
test('should format for tomorrow', () => {
const then = new Date('2025-07-26T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('tomorrow');
});
});
describe('secondsToTime', () => {
test('should format seconds only', () => {
expect(secondsToTime(45)).toBe('0:45');
});
test('should format minutes and seconds', () => {
expect(secondsToTime(156)).toBe('2:36');
});
test('should format hours, minutes, and seconds', () => {
expect(secondsToTime(3756)).toBe('1:02:36');
});
test('should format with fixed minutes', () => {
expect(secondsToTime(3726, true)).toBe('1:02:06');
expect(secondsToTime(126, true)).toBe('02:06');
});
test('should handle non-number input', () => {
expect(secondsToTime('abc')).toBe('');
});
});
describe('bytesToHumanSize', () => {
test('should format 0 bytes', () => {
expect(bytesToHumanSize(0)).toBe('0 Bytes');
});
test('should format bytes', () => {
expect(bytesToHumanSize(500)).toBe('500 Bytes');
});
test('should format kilobytes', () => {
expect(bytesToHumanSize(1500)).toBe('1.46 KB');
});
test('should format megabytes', () => {
expect(bytesToHumanSize(1500000)).toBe('1.43 MB');
});
test('should format gigabytes', () => {
expect(bytesToHumanSize(1500000000)).toBe('1.40 GB');
});
});
describe('shortenFileName', () => {
test('should not shorten if not needed', () => {
expect(shortenFileName('shortname.txt', 20)).toBe('shortname.txt');
});
test('should shorten a long file name', () => {
const longName = 'this_is_a_very_long_file_name_that_needs_shortening.txt';
expect(shortenFileName(longName, 20)).toBe('this_is_a…ening.txt');
expect(shortenFileName(longName, 20).length).toBeLessThanOrEqual(20);
});
test('should handle non-string input', () => {
expect(shortenFileName(null, 20)).toBeNull();
expect(shortenFileName(undefined, 20)).toBeUndefined();
});
});
describe('idToColorClass', () => {
test('should generate light background color class', () => {
expect(idToColorClass('usr123', true, false)).toMatch(/lt-bg-\d+/);
});
test('should generate dark background color class', () => {
expect(idToColorClass('usr123', false, false)).toMatch(/dk-bg-\d+/);
});
test('should generate light foreground color class', () => {
expect(idToColorClass('usr123', true, true)).toMatch(/lt-fg-\d+/);
});
test('should generate dark foreground color class', () => {
expect(idToColorClass('usr123', false, true)).toMatch(/dk-fg-\d+/);
});
test('should be deterministic', () => {
expect(idToColorClass('usr123', true, false)).toBe('lt-bg-2');
expect(idToColorClass('grpABC', false, true)).toBe('dk-fg-3');
});
});
describe('flagEmoji', () => {
test('should convert country code to flag', () => {
expect(flagEmoji('US')).toBe('🇺🇸');
});
test('should handle lowercase country code', () => {
expect(flagEmoji('jp')).toBe('🇯🇵');
});
});
describe('truncateString', () => {
test('should not truncate if not needed', () => {
expect(truncateString('short', 10)).toBe('short');
});
test('should truncate and add ellipsis for long strings', () => {
expect(truncateString('this is a long string', 10)).toBe('this is a…');
});
test('should handle RTL strings (Arabic)', () => {
expect(truncateString('هذا مثال لنص عربي طويل يحتاج.', 10)).toBe('…هذا مثال ');
});
test('should handle RTL strings (Hebrew)', () => {
expect(truncateString('זה דוגמה לטקסט עברי ארוך', 10)).toBe('…זה דוגמה ');
});
test('should handle RTL strings (Persian/Farsi)', () => {
expect(truncateString('این یک نمونه متن فارسی طولانی است', 10)).toBe('…این یک نم');
});
test('should handle RTL strings (Urdu)', () => {
expect(truncateString('یہ اردو زبان کا ایک طویل جملہ ہے', 10)).toBe('…یہ اردو ز');
});
test('should handle RTL strings (Arabic with mixed punctuation)', () => {
expect(truncateString('النص العربي الطويل! متحف الفن', 10)).toBe('…النص العر');
});
test('should handle RTL strings (Hebrew with English mixed)', () => {
expect(truncateString('טקסט עברי עם English מעורב', 10)).toBe('…טקסט עברי');
});
});
+65 -17
View File
@@ -1,6 +1,6 @@
// Odds and ends
import Tinode from 'tinode-sdk';
import { Tinode } from 'tinode-sdk';
// Make shortcut icon appear with a green dot + show unread count in title.
export function updateFavicon(count) {
@@ -89,7 +89,7 @@ export function arrayEqual(a, b) {
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 val.replaceAll(/[- ().]*/g, '');
}
return null;
}
@@ -97,7 +97,7 @@ export function asPhone(val) {
// 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)) {
if (/^[a-z0-9_.+-]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(val)) {
return val;
}
return null;
@@ -108,9 +108,12 @@ export function asEmail(val) {
// example.html - ok
// https:example.com - not ok.
// http:/example.com - not ok.
// //example.com or \\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);
// Replacing backslashes with forward slashes to mimic JS URL parser then testing for
// 'scheme:' and '//' with optional space at the start.
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url.replace(/\\/g, '/'));
}
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
@@ -121,7 +124,10 @@ export function sanitizeUrl(url, allowedSchemes) {
}
// Strip control characters and whitespace. They are not valid URL characters anyway.
url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
url = url.replace(/[^\x21-\x7E]/gmi, '').trim();
// Replace backslashes with forward slashes. They will be replaced in the URL parser anyway.
url = url.replace(/\\/g, '/');
// Relative URLs are safe.
// Relative URL does not start with ':', abcd123: or '//'.
@@ -144,10 +150,10 @@ export function sanitizeUrl(url, allowedSchemes) {
return url;
}
// Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or
// 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 start with a 'data:image/XXXX;base64,'.
export function sanitizeImageUrl(url) {
// 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;
}
@@ -157,20 +163,57 @@ export function sanitizeImageUrl(url) {
return sanitizedUrl;
}
// Is this a data: URL of an image?
if (/data:image\/[a-z0-9.-]+;base64,/i.test(url.trim())) {
// 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;
}
// Append query parameter 'asatt=1' to the URL.
// It will cause Tinode server to add 'Content-Disposition: attachment' header when serving it.
// The URL is a string. The URL here is always absolute.
export function urlAsAttachment(url) {
// TODO: check if URL is local or remote, i.e. compare to window.location.origin.
/*
if (typeof window.location == 'object') {
if (!url.startsWith(window.location.origin)) {
return url;
}
}
*/
// Check if the URL is a data or blob. Do not alter such URLs.
if (url.startsWith('data:') || url.startsWith('blob:')) {
return url;
}
// Split URL into host+path, query, fragment.
let query = '', fragment = '';
const idxF = url.indexOf('#');
if (idxF > 0) {
fragment = url.substring(idxF+1);
url = url.substring(0, idxF);
}
const idxQ = url.indexOf('?');
if (idxQ > 0) {
query = url.substring(idxQ+1);
url = url.substring(0, idxQ);
}
// Add parameter and reassemble.
const params = new URLSearchParams(query);
params.append('asatt', '1');
return `${url}?${params.toString()}` + (fragment ? `#${fragment}` : '');
}
// 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
@@ -183,20 +226,25 @@ export function deliveryMarker(received) {
}
// 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 = new Promise((resolve, reject) => {
promise.then(
result => hasCanceled ? reject({isCanceled: true}) : resolve(result),
error => hasCanceled ? reject({isCanceled: true}) : reject(error)
);
});
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( ) {
cancel() {
hasCanceled = true;
},
};
};
+127
View File
@@ -0,0 +1,127 @@
import {
arrayEqual,
asEmail,
asPhone,
isUrlRelative,
sanitizeUrl,
sanitizeUrlForMime,
urlAsAttachment
} from './utils';
test('asPhone', () => {
expect(asPhone('123-456-7890')).toBe('1234567890');
expect(asPhone('(123) 456-7890')).toBe('1234567890');
expect(asPhone('+1 (123) 456-7890')).toBe('+11234567890');
expect(asPhone('123.456.7890')).toBe('1234567890');
expect(asPhone('123 456 7890')).toBe('1234567890');
expect(asPhone('123-456-78901')).toBe('12345678901');
expect(asPhone('')).toBeNull();
expect(asPhone('abc-def-ghij')).toBeNull();
expect(asPhone('123-456-789')).toBeNull();
expect(asPhone('123-4i6-78901')).toBeNull();
});
test('arrayEqual', () => {
expect(arrayEqual([1, 2, 3], [3, 2, 1])).toBe(true);
expect(arrayEqual([1, 2, 3], [4, 5, 6])).toBe(false);
expect(arrayEqual([1, 2, 3], [1, 2, 3, 4])).toBe(false);
expect(arrayEqual([1, 2, 3], [1, 2])).toBe(false);
expect(arrayEqual([], [])).toBe(true);
expect(arrayEqual([1], [1])).toBe(true);
expect(arrayEqual([1], [2])).toBe(false);
expect(arrayEqual(null, [1, 2, 3])).toBe(false);
expect(arrayEqual([1, 2, 3], null)).toBe(false);
expect(arrayEqual(null, null)).toBe(true);
expect(arrayEqual(undefined, undefined)).toBe(true);
expect(arrayEqual([1, 2, 3], undefined)).toBe(false);
expect(arrayEqual([1, 2, 3], [1, 2, '3'])).toBe(false);
});
test('asEmail', () => {
expect(asEmail('test@example.com')).toBe('test@example.com');
expect(asEmail('test@mail.example.com')).toBe('test@mail.example.com');
expect(asEmail('test@e-mail.example.com')).toBe('test@e-mail.example.com');
expect(asEmail(' test@example.com ')).toBe('test@example.com');
expect(asEmail('test.email+alex@leetcode.com')).toBe('test.email+alex@leetcode.com');
expect(asEmail('test.email@sub.example.com')).toBe('test.email@sub.example.com');
expect(asEmail('test.email@sub.example.co.uk')).toBe('test.email@sub.example.co.uk');
expect(asEmail('invalid-email')).toBeNull();
expect(asEmail('invalid-email@')).toBeNull();
expect(asEmail('invalid-email@example')).toBeNull();
expect(asEmail('invalid-email@example.')).toBeNull();
expect(asEmail('invalid-email@example..com')).toBeNull();
expect(asEmail('invalid-email@.example.com')).toBeNull();
expect(asEmail('invalid-email@example.com.')).toBeNull();
expect(asEmail('invalid-email@@example.com.')).toBeNull();
expect(asEmail('invalid/email@example.com.')).toBeNull();
});
test('isUrlRelative', () => {
expect(isUrlRelative('example.html')).toBe(true);
expect(isUrlRelative('https:example.com')).toBe(false);
expect(isUrlRelative('http:/example.com')).toBe(false);
expect(isUrlRelative(' \n https://example.com')).toBe(false);
expect(isUrlRelative('//example.com')).toBe(false);
expect(isUrlRelative('\\example.com')).toBe(true);
expect(isUrlRelative('example.com')).toBe(true);
expect(isUrlRelative('/example')).toBe(true);
expect(isUrlRelative('ftp://example.com')).toBe(false);
expect(isUrlRelative('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA')).toBe(false);
});
test('sanitizeUrl', () => {
// Test with valid URLs
expect(sanitizeUrl('http://example.com')).toBe('http://example.com');
expect(sanitizeUrl('https://example.com')).toBe('https://example.com');
expect(sanitizeUrl('blob:http://example.com')).toBe('blob:http://example.com');
expect(sanitizeUrl('example.html')).toBe('example.html');
// Test with invalid URLs
expect(sanitizeUrl('ftp://example.com')).toBeNull();
expect(sanitizeUrl('javascript:alert(1)')).toBeNull();
expect(sanitizeUrl('data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==')).toBeNull();
// Test with allowed schemes
expect(sanitizeUrl('ftp://example.com', ['ftp'])).toBe('ftp://example.com');
expect(sanitizeUrl('ftps://example.com', ['ftp', 'ftps'])).toBe('ftps://example.com');
// Test with non-string input
expect(sanitizeUrl(null)).toBe(null);
expect(sanitizeUrl(123)).toBe(123);
// Test with control characters and whitespace
expect(sanitizeUrl(' \n https://example.com')).toBe('https://example.com');
expect(sanitizeUrl('http:\\\\example.com')).toBe('http://example.com');
});
test('sanitizeUrlForMime', () => {
expect(sanitizeUrlForMime('data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==', 'image')).toBeNull();
// Test with empty and null URLs
expect(sanitizeUrlForMime('', 'image')).toBeNull();
expect(sanitizeUrlForMime(null, 'image')).toBeNull();
// Test with valid data URL but wrong mime type
expect(sanitizeUrlForMime('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA', 'text')).toBeNull();
});
test('urlAsAttachment', () => {
// Test with a simple URL
expect(urlAsAttachment('http://example.com')).toBe('http://example.com?asatt=1');
// Test with a URL that already has a query parameter
expect(urlAsAttachment('http://example.com?param=value')).toBe('http://example.com?param=value&asatt=1');
// Test with a URL that has a fragment
expect(urlAsAttachment('http://example.com#fragment')).toBe('http://example.com?asatt=1#fragment');
// Test with a URL that has both query parameters and a fragment
expect(urlAsAttachment('http://example.com?param=value#fragment')).toBe('http://example.com?param=value&asatt=1#fragment');
// Test with a data URL
expect(urlAsAttachment('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA')).toBe('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA');
// Test with a blob URL
expect(urlAsAttachment('blob:http://example.com/550e8400-e29b-41d4-a716-446655440000')).toBe('blob:http://example.com/550e8400-e29b-41d4-a716-446655440000');
});
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.18.0-beta1";
export const PACKAGE_VERSION = "0.25.0-beta1";
+146
View File
@@ -0,0 +1,146 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
export default class AccGeneralView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
colorSchema: props.colorSchema || 'light dark',
textSize: (props.textSize * 10) || '100',
sendOnEnter: props.sendOnEnter || 'plain',
};
this.isMac = ((navigator.userAgentData && navigator.userAgentData.platform)
|| navigator.platform).toLowerCase().startsWith('mac');
this.handleColorSchemaSelected = this.handleColorSchemaSelected.bind(this);
this.handleTextSizeChanged = this.handleTextSizeChanged.bind(this);
this.handleSendOnEnterSelected = this.handleSendOnEnterSelected.bind(this);
}
handleColorSchemaSelected(e) {
this.setState({colorSchema: e.currentTarget.value});
this.props.onChangeColorSchema(e.currentTarget.value);
}
handleTextSizeChanged(e) {
// In %%.
this.setState({textSize: e.currentTarget.value});
// Convert %% to pt, 100% = 10pt.
this.props.onTextSizeChanged((e.currentTarget.value / 10) | 0);
}
handleSendOnEnterSelected(e) {
this.setState({sendOnEnter: e.currentTarget.value});
this.props.onSendOnEnterChanged(e.currentTarget.value);
}
render() {
return (
<div id="settings-form" className="scrollable-panel">
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_color_schema" defaultMessage="Theme:"
description="Label for selecting color scheme (dark, light) in Settings" />
</label>
</div>
<div className="panel-form-row">
<ul className="quoted">
<li key="system">
<input type="radio" id="system" name="color-scheme-select" value="light dark"
checked={this.state.colorSchema == 'light dark'}
onChange={this.handleColorSchemaSelected} />&nbsp;
<label htmlFor="system">
<FormattedMessage id="color_schema_system" defaultMessage="System default" description="Name of the color schema"/>&nbsp;
<img src="../img/routine.svg" style={{verticalAlign: 'top', width: '1.6rem', height: '1.6rem'}}/>
</label>
</li>
<li key="light">
<input type="radio" id="light" name="color-scheme-select" value="light"
checked={this.state.colorSchema == 'light'}
onChange={this.handleColorSchemaSelected} />&nbsp;
<label htmlFor="light">
<FormattedMessage id="color_schema_light" defaultMessage="Light" description="Name of the color schema"/>&nbsp;
<i className="material-icons orange large">light_mode</i>
</label>
</li>
<li key="dark">
<input type="radio" id="dark" name="color-scheme-select" value="dark"
checked={this.state.colorSchema == 'dark'}
onChange={this.handleColorSchemaSelected} />&nbsp;
<label htmlFor="dark">
<FormattedMessage id="color_schema_dark" defaultMessage="Dark" description="Name of the color schema"/>&nbsp;
<i className="material-icons blue large">dark_mode</i>
</label>
</li>
</ul>
</div>
<div className="hr" />
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_text_size" defaultMessage="Text size:"
description="Label adjusting text size" />
</label>
</div>
<div className="panel-form-column">
<div className="panel-form-row">
<input type="range" id="text_size" name="text_size" min="80" max="120" step="10"
list="text_size_options" value={this.state.textSize} onChange={this.handleTextSizeChanged} />
</div>
<div className="panel-form-row">
<datalist id="text_size_options">
<option value="80" label="80%" />
<option value="90" label="90%" />
<option value="100" label="100%" />
<option value="110" label="110%" />
<option value="120" label="120%" />
</datalist>
</div>
</div>
<div className="hr" />
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_keyboard" defaultMessage="Keyboard:"
description="Label for send on enter settings" />
</label>
</div>
<div className="panel-form-row">
<ul className="quoted">
<li key="plain">
<input type="radio" id="plain" name="send-select" value="plain"
checked={this.state.sendOnEnter == 'plain'}
onChange={this.handleSendOnEnterSelected} />&nbsp;
<label htmlFor="send_plain">
<FormattedMessage id="send_plain" defaultMessage="Send on Enter"
description="Config option to send message on hitting Enter"/>&nbsp;
</label>
<div className="panel-form-row">
<span className="small gray">
<FormattedMessage id="send_plain_explained" defaultMessage="Press Shift + Enter for new line"
description="Explanation how to enter newline when [Send on Enter] is enabled" />
</span>
</div>
</li>
<li key="command">
<input type="radio" id="command" name="send-select" value="command"
checked={this.state.sendOnEnter == 'command'}
onChange={this.handleSendOnEnterSelected} />&nbsp;
<label htmlFor="send_command">
<FormattedMessage id="send_command" defaultMessage="Send on {key}"
values={{key: this.isMac ? '⌘ + Enter' : 'Ctrl + Enter'}}
description="Config option to send message on hitting CTRL(or Cmd)-Enter"/>&nbsp;
</label>
<div className="panel-form-row">
<span className="small gray">
<FormattedMessage id="send_command_explained" defaultMessage="Press Enter for new line"
description="Explanation how to enter newline when [Send on CTRL-Enter] is enabled" />
</span>
</div>
</li>
</ul>
</div>
</div>
);
}
};
+1 -11
View File
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
export default class EditAccountView extends React.PureComponent {
export default class AccNotificationsView extends React.PureComponent {
constructor(props) {
super(props);
@@ -16,8 +16,6 @@ export default class EditAccountView extends React.PureComponent {
this.props.onToggleMessageSounds(checked);
} else if (what == 'alert') {
this.props.onTogglePushNotifications(checked);
} else if (what == 'incognito') {
this.props.onToggleIncognitoMode(checked);
}
}
@@ -47,14 +45,6 @@ export default class EditAccountView extends React.PureComponent {
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>
);
}
+23 -2
View File
@@ -2,6 +2,7 @@
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
@@ -42,6 +43,7 @@ class AccSecurityView extends React.Component {
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
}
handlePasswordUpdate(pwd) {
@@ -70,13 +72,17 @@ class AccSecurityView extends React.Component {
this.setState(newState);
}
handleCheckboxClick(what, checked) {
this.props.onToggleIncognitoMode(checked);
}
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
(_ => this.props.onDeleteAccount()), // onConfirm
null, // use default text "OK"
true, // Show Reject button
null // use default text "Cancel"
@@ -109,6 +115,21 @@ class AccSecurityView extends React.Component {
</div>
</div>
<div className="hr" />
<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 className="panel-form-row">
<span className="small gray">
<FormattedMessage id="incognito_mode_description" defaultMessage="Hide your online status and typing indicators."
description="Description for incognito mode toggle" />
</span>
</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"
@@ -124,7 +145,7 @@ class AccSecurityView extends React.Component {
<div>
<label className="small">
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
description="Label for default access mode" />
description="Label for default access mode" />
</label>
</div>
<div className="quoted">
+1 -1
View File
@@ -2,7 +2,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk';
import { Tinode } from 'tinode-sdk';
import { APP_NAME, LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
+94 -7
View File
@@ -1,15 +1,18 @@
// 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 CredentialEdit from '../widgets/credential-edit.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { truncateString } from '../lib/strformat.js';
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
function _clip(str, length) {
return str && str.substring(0, length);
return truncateString(str, length);
}
export default class AccountSettingsView extends React.Component {
@@ -20,15 +23,85 @@ export default class AccountSettingsView extends React.Component {
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)
avatar: makeImageUrl(me.public ? me.public.photo : null),
credentials: me.getCredentials() || [],
credEdit: undefined,
alias: ((me.tags() || []).find(tag => tag.startsWith('alias:')) || '').substring(6),
};
}
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}
onCredAdd={this.props.onCredAdd}
onCredConfirm={this.props.onCredConfirm}
onCancel={_ => this.setState({credEdit: undefined})}
onError={this.props.onError} />
);
}
// Count how many times each method is validated.
const validated = {};
this.state.credentials.forEach(cred => {
if (cred.done) {
validated[cred.meth] = (validated[cred.meth] || 0) + 1;
}
});
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;
{!cred.done || validated[cred.meth] > 1 || this.props.reqCredMethod != cred.meth ?
<a href="#" onClick={e => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}>
<i className="material-icons">delete</i>
</a>
:
null}
</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');}}>
<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>
@@ -52,6 +125,12 @@ export default class AccountSettingsView extends React.Component {
description="Label for user address (ID)" /></label>&nbsp;
<tt>{this.props.myUserId}</tt>
</div>
{this.state.alias ?
<div className="group">
<label className="small"><FormattedMessage id="label_alias" defaultMessage="Alias:"
description="Label for user or topic alias" /></label>&nbsp;
<tt>@{this.state.alias}</tt>
</div> : null}
<div className="group">
<BadgeList trustedBadges={this.props.trustedBadges} />
</div>
@@ -61,20 +140,28 @@ export default class AccountSettingsView extends React.Component {
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
</label>
<div>{this.state.description}</div>
<div className="quoted">{this.state.description}</div>
</div> : null}
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onNavigate('notif');}}>
<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('acc_general');}}>
<i className="material-icons">tune</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_general"
defaultMessage="General" description="Sidepanel title for AccGeneralView." />
</a>
<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');}}>
<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');}}>
<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>
+7 -2
View File
@@ -27,8 +27,12 @@ export default class ContactsView extends React.Component {
const contacts = [];
let unreadThreads = 0;
let archivedCount = 0;
props.chatList.map((c) => {
const me = props.tinode.getMeTopic();
props.chatList.forEach(c => {
const blocked = c.acs && !c.acs.isJoiner();
c.pinned = me.pinnedTopicRank(c.topic);
// Show only blocked contacts only when props.blocked == true.
if (blocked && props.blocked) {
contacts.push(c);
@@ -50,7 +54,8 @@ export default class ContactsView extends React.Component {
});
contacts.sort((a, b) => {
return (b.touched || 0) - (a.touched || 0);
const pin = b.pinned - a.pinned;
return pin != 0 ? pin : (b.touched || 0) - (a.touched || 0);
});
if (archivedCount > 0) {
+123 -20
View File
@@ -1,15 +1,20 @@
// Account registration.
import React from 'react';
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 { truncateString } from '../lib/strformat.js';
import { MAX_TITLE_LENGTH } from '../config.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) {
@@ -18,20 +23,35 @@ export default class CreateAccountView extends React.PureComponent {
this.state = {
login: '',
password: '',
meth: '',
email: '',
tel: '',
fn: '', // full/formatted name
imageDataUrl: null,
errorCleared: false,
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) {
@@ -43,15 +63,23 @@ export default class CreateAccountView extends React.PureComponent {
}
handleEmailChange(e) {
this.setState({email: e.target.value})
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(img) {
this.setState({imageDataUrl: img});
handleImageChanged(mime, img) {
this.setState({newAvatar: img, newAvatarMime: mime});
if (!img) {
// User cleared the image.
this.setState({imageUrl: null, uploadUrl: null});
}
}
handleToggleSaveToken() {
@@ -61,15 +89,75 @@ export default class CreateAccountView extends React.PureComponent {
handleSubmit(e) {
e.preventDefault();
this.setState({errorCleared: false});
this.props.onCreateAccount(
this.state.login.trim(),
this.state.password.trim(),
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl),
{'meth': 'email', 'val': this.state.email});
theCard(truncateString(this.state.fn.trim(), 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({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';
@@ -78,7 +166,7 @@ export default class CreateAccountView extends React.PureComponent {
return (
<form className="panel-form-column" onSubmit={this.handleSubmit}>
<div className="panel-form-row">
<div className="panel-form-column">
<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"
@@ -93,7 +181,8 @@ export default class CreateAccountView extends React.PureComponent {
</div>
<AvatarUpload
tinode={this.props.tinode}
onImageChanged={this.handleImageChanged}
avatar={this.state.imageUrl}
onImageUpdated={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="panel-form-row">
@@ -103,13 +192,27 @@ export default class CreateAccountView extends React.PureComponent {
value={this.state.fn} onChange={this.handleFnChange} required/>
}</FormattedMessage>
</div>
<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 == '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} />
@@ -119,7 +222,7 @@ export default class CreateAccountView extends React.PureComponent {
}</FormattedMessage>
</div>
<div className="dialog-buttons">
<button className={submitClasses} type="submit">
<button className={submitClasses} type="submit" disabled={this.state.buttonDisabled}>
<FormattedMessage id="button_sign_up" defaultMessage="Sign up"
description="Create account button [Sign Up]" />
</button>
+167 -50
View File
@@ -1,8 +1,8 @@
// InfoView: panel with topic/user info.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { FormattedMessage, FormattedNumber, defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk';
import { AccessMode, Tinode } from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx';
@@ -12,13 +12,16 @@ 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 TopicCommon from '../widgets/topic-common.jsx';
import ShowQRCode from '../widgets/show-qrcode.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 { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH,
NO_ACCESS_MODE, TOAST_DURATION } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, theCard } from '../lib/utils.js';
import { theCard } from '../lib/utils.js';
import { truncateString } from '../lib/strformat.js';
const messages = defineMessages({
info: {
@@ -76,12 +79,28 @@ const messages = defineMessages({
defaultMessage: 'Edit permissions',
description: 'Menu item [Edit permissions]'
},
qrcode: {
id: 'scan_qr_code',
defaultMessage: 'Scan QR Code',
description: 'Title for scanning QR code'
},
text_copied: {
id: 'text_copied',
defaultMessage: 'Copied to clipboard',
description: 'Notification that text has been copied to clipboard'
},
self_topic_name: {
id: 'self_topic_name',
defaultMessage: 'Saved messages',
description: 'Name of self topic for UI',
},
self_topic_comment: {
id: 'self_topic_comment',
defaultMessage: 'Notes, messages, links, files saved for posterity',
description: 'Comment for self topic for UI',
},
});
function _clip(str, length) {
return str && str.substring(0, length);
}
class InfoView extends React.Component {
constructor(props) {
super(props);
@@ -95,6 +114,7 @@ class InfoView extends React.Component {
muted: false,
address: null,
groupTopic: undefined,
isSelf: false,
channel: undefined,
fullName: undefined,
description: undefined,
@@ -114,6 +134,7 @@ class InfoView extends React.Component {
previousSubsUpdated: undefined,
};
this.propsUpdated = this.propsUpdated.bind(this);
this.resetSubs = this.resetSubs.bind(this);
this.resetDesc = this.resetDesc.bind(this);
this.resetTags = this.resetTags.bind(this);
@@ -124,6 +145,8 @@ class InfoView extends React.Component {
this.handleUnarchive = this.handleUnarchive.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleShowQRCode = this.handleShowQRCode.bind(this);
this.handleCopyToClipboard = this.handleCopyToClipboard.bind(this);
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this);
@@ -131,8 +154,7 @@ class InfoView extends React.Component {
this.handleBackNavigate = this.handleBackNavigate.bind(this);
}
// No need to separately handle component mount.
componentDidUpdate(props) {
propsUpdated(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
@@ -154,6 +176,15 @@ class InfoView extends React.Component {
}
}
// Must provide this for lazy loaded components.
componentDidMount() {
this.propsUpdated(this.props);
}
componentDidUpdate(props) {
this.propsUpdated(props);
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
@@ -199,6 +230,8 @@ class InfoView extends React.Component {
}
}
const isSelf = topic.isSelfType();
this.setState({
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
@@ -206,14 +239,19 @@ class InfoView extends React.Component {
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(),
fullName: _clip(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
description: _clip(topic.public ? topic.public.note : undefined, MAX_TOPIC_DESCRIPTION_LENGTH),
fullName: isSelf ?
props.intl.formatMessage(messages.self_topic_name) :
truncateString(topic.public && topic.public.fn, MAX_TITLE_LENGTH),
description: isSelf ?
props.intl.formatMessage(messages.self_topic_comment) :
truncateString(topic.public && topic.public.note, MAX_TOPIC_DESCRIPTION_LENGTH),
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
trustedBadges: badges,
private: _clip(topic.private ? topic.private.comment : null, MAX_TITLE_LENGTH),
private: truncateString(topic.private && topic.private.comment, MAX_TITLE_LENGTH),
archived: topic.isArchived(),
address: topic.name,
groupTopic: topic.isGroupType(),
isSelf: isSelf,
channel: topic.isChannelType() || topic.chan,
access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined,
@@ -259,7 +297,7 @@ class InfoView extends React.Component {
}
}
handleImageChanged(img) {
handleImageChanged(mime, img) {
this.setState({avatar: img});
this.props.onTopicDescUpdate(this.props.topic, theCard(null, img || Tinode.DEL_CHAR), null);
}
@@ -311,7 +349,7 @@ class InfoView extends React.Component {
toSkip = 'O';
} else {
// Allow accepting any of 'ASDO' permissions but don't allow asking for them.
toSkip = Tinode.AccessMode.encode(Tinode.AccessMode.diff('ASDO', this.state.modeGiven));
toSkip = AccessMode.encode(AccessMode.diff('ASDO', this.state.modeGiven));
if (this.state.channel) {
// Channels are read-only.
toSkip += 'W';
@@ -372,6 +410,20 @@ class InfoView extends React.Component {
this.props.onNavigate(`perm/${which}`);
}
handleShowQRCode(e) {
e.preventDefault();
this.props.onNavigate('qrcode');
}
handleCopyToClipboard(e, text) {
e.preventDefault();
navigator.clipboard.writeText(text).then(_ => {
this.setState({toast: this.props.intl.formatMessage(messages.text_copied)});
setTimeout(_ => { this.setState({toast: ''}); }, TOAST_DURATION);
});
}
handleShowAddMembers(e) {
e.preventDefault();
this.props.onInitFind();
@@ -414,11 +466,10 @@ class InfoView extends React.Component {
}
const isMe = this.props.tinode.isMe(params.topicName);
const menuItems = [
{title: formatMessage(messages.edit_permissions), handler: () => {
this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName);
}}
];
const menuItems = [{
title: formatMessage(messages.edit_permissions),
handler: _ => this.handleLaunchPermissionsEditor(isMe ? 'want' : 'user', params.topicName)
}];
if (!isMe) {
menuItems.push('member_delete');
}
@@ -442,8 +493,12 @@ class InfoView extends React.Component {
const panelTitle = formatMessage((view == 'perm' ? messages['perm_' + args[0]] : messages[view])
|| messages['info']);
const topic = this.props.tinode.getTopic(this.state.topic);
const alias = topic && topic.alias();
return (
<div id="info-view">
<div className={`toast${this.state.toast ? ' show' : ''}`}>{this.state.toast}</div>
<div className="caption-panel" id="info-caption-panel">
<div className="panel-title" id="info-title">{panelTitle}</div>
<div>
@@ -481,11 +536,12 @@ class InfoView extends React.Component {
onCancel={this.handleBackNavigate} />
:
view == 'general' ?
<TopicCommon
<TopicCommonView
tinode={this.props.tinode}
topic={this.props.topic}
reqCredMethod={this.props.reqCredMethod}
onCredAdd={this.props.onCredAdd}
onTopicTagsUpdateRequest={this.props.onTopicTagsUpdateRequest}
onUpdateTagsRequest={this.props.onTopicTagsUpdateRequest}
onCredConfirm={this.props.onCredConfirm}
onCredDelete={this.props.onCredDelete}
onUpdateTopicDesc={this.props.onTopicDescUpdateRequest}
@@ -518,12 +574,20 @@ class InfoView extends React.Component {
onLaunchPermissionsEditor={this.handleLaunchPermissionsEditor}
onNavigate={this.props.onNavigate} />
:
view == 'qrcode' ?
<ShowQRCode
uri={Tinode.URI_TOPIC_ID_PREFIX + this.props.topic}
onCancel={this.handleBackNavigate} />
:
<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>
{!this.state.isSelf ?
<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>
: null
}
<center>
<AvatarUpload
tinode={this.props.tinode}
@@ -550,14 +614,61 @@ class InfoView extends React.Component {
</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.isSelf ?
<>
<div className="panel-form-row">
<div>
<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 style={{marginLeft: 'auto'}}>
&nbsp;<a href="#" onClick={e => {this.handleCopyToClipboard(e, this.state.address);}}>
<i className="material-icons">content_copy</i>
</a>&nbsp;
&nbsp;<a href="#" onClick={this.handleShowQRCode}>
<i className="material-icons">qr_code</i>
</a>&nbsp;
</div>
</div>
{alias ?
<div className="panel-form-row">
<div>
<label className="small"><FormattedMessage id="label_alias" defaultMessage="Alias:"
description="Label for user or topic alias" /></label>&nbsp;
<tt>{alias}</tt>
</div>
<div style={{marginLeft: 'auto'}}>
&nbsp;<a href="#" onClick={e => {this.handleCopyToClipboard(e, alias);}}>
<i className="material-icons">content_copy</i>
</a>&nbsp;
&nbsp;<i className="material-icons" style={{opacity: 0}}>qr_code</i>&nbsp;
</div>
</div>
: null}
</>
: null
}
{this.state.trustedBadges.length > 0 ?
<div className="group">
<BadgeList trustedBadges={this.state.trustedBadges} />
</div>
: null
}
{this.state.groupTopic && topic.subcnt > 0 ?
<div className="group"><label className="small">
{this.state.channel ?
<FormattedMessage id="label_subscriber_count"
defaultMessage="Subscribers:"
description="Label for the count of channel subscribers" />
:
<FormattedMessage id="label_member_count"
defaultMessage="Members:"
description="Label for the count of group topic members" />
}
</label>&nbsp;<FormattedNumber value={topic.subcnt}/></div>
: null
}
{this.state.description ?
<div className="group">
<label className="small">
@@ -567,14 +678,17 @@ class InfoView extends React.Component {
<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.isSelf ?
<><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></>
: null
}
{this.state.archived ?
<div className="panel-form-row">
<label>
@@ -586,13 +700,16 @@ class InfoView extends React.Component {
:
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.isSelf ?
<><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></>
: null
}
{this.state.groupTopic && this.state.sharer ?
<>
<div className="hr" />
-3
View File
@@ -5,8 +5,6 @@ import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import VisiblePassword from '../widgets/visible-password.jsx';
import LocalStorageUtil from '../lib/local-storage.js';
export default class LoginView extends React.Component {
constructor(props) {
super(props);
@@ -95,4 +93,3 @@ export default class LoginView extends React.Component {
);
}
};
/* END Login */
+1 -1
View File
@@ -2,7 +2,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk';
import { Tinode } from 'tinode-sdk';
import { APP_NAME } from '../config.js';
File diff suppressed because it is too large Load Diff
+35 -13
View File
@@ -2,7 +2,7 @@
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk';
import { Tinode } from 'tinode-sdk';
import ContactList from '../widgets/contact-list.jsx';
import NewTopicById from '../widgets/new-topic-by-id.jsx';
@@ -10,7 +10,7 @@ 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';
import { asEmail, asPhone, theCard } from '../lib/utils.js';
const messages = defineMessages({
search_for_contacts: {
@@ -25,7 +25,7 @@ const messages = defineMessages({
},
search_placeholder: {
id: "search_placeholder",
defaultMessage: "List like email:alice@example.com, tel:17025550003...",
defaultMessage: "List like alice@example.com, +17025550003...",
description: "Placeholder in contacts search field"
},
});
@@ -35,13 +35,13 @@ class NewTopicView extends React.Component {
super(props);
this.state = {
tabSelected: 'p2p',
tabSelected: 'find',
searchQuery: null
};
this.handleTabClick = this.handleTabClick.bind(this);
this.handleSearchContacts = this.handleSearchContacts.bind(this);
this.handleContactSelected = this.handleContactSelected.bind(this);
this.handleSearchResultSelected = this.handleSearchResultSelected.bind(this);
this.handleNewGroupSubmit = this.handleNewGroupSubmit.bind(this);
this.handleGroupByID = this.handleGroupByID.bind(this);
}
@@ -57,14 +57,34 @@ class NewTopicView extends React.Component {
}
handleSearchContacts(query) {
query = query.trim() || Tinode.DEL_CHAR;
if (!/[\s,:]/.test(query) && query != Tinode.DEL_CHAR) {
// No colons, spaces or commas, not DEL char. Try as email, phone, or alias.
const email = asEmail(query);
if (email) {
query = `${Tinode.TAG_EMAIL}${email}`;
} else {
const tel = asPhone(query);
if (tel) {
query = `${Tinode.TAG_PHONE}${tel}`;
} else {
if (query[0] == '@') {
// Strip the leading '@' sign if present.
query = query.substring(1);
}
// 'alice' -> 'alias:alice,alice'
query = `${Tinode.TAG_ALIAS}${query},${query}`;
}
}
}
this.props.onSearchContacts(query);
this.setState({searchQuery: Tinode.isNullValue(query) ? null : query});
}
handleContactSelected(uid) {
if (this.state.tabSelected == 'p2p') {
handleSearchResultSelected(topicName) {
if (this.state.tabSelected == 'find') {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(uid);
this.props.onCreateTopic(topicName);
}
}
@@ -87,8 +107,8 @@ class NewTopicView extends React.Component {
return (
<div className="flex-column">
<ul className="tabbar">
<li className={this.state.tabSelected === "p2p" ? "active" : null}>
<a href="#" data-id="p2p" onClick={this.handleTabClick}>
<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>
@@ -109,14 +129,15 @@ class NewTopicView extends React.Component {
{this.state.tabSelected === 'grp' ?
<NewTopicGroup
tinode={this.props.tinode}
onSubmit={this.handleNewGroupSubmit} /> :
onSubmit={this.handleNewGroupSubmit}
onError={this.props.onError} /> :
this.state.tabSelected === 'byid' ?
<NewTopicById
myURI={Tinode.URI_TOPIC_ID_PREFIX + this.props.tinode.getCurrentUserID()}
onSubmit={this.handleGroupByID}
onError={this.props.onError} /> :
<div className="flex-column">
<SearchContacts
type="p2p"
placeholder={search_placeholder}
onSearchContacts={this.handleSearchContacts} />
<ContactList
@@ -124,10 +145,11 @@ class NewTopicView extends React.Component {
contacts={this.props.searchResults}
myUserId={this.props.myUserId}
emptyListMessage={no_contacts_placeholder}
showSelfTopic={!this.state.searchQuery}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleContactSelected} />
onTopicSelected={this.handleSearchResultSelected} />
</div>}
</div>
);
+180 -57
View File
@@ -1,43 +1,102 @@
// A password reset form.
import React from 'react';
import { FormattedMessage } from 'react-intl';
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';
export default class PasswordResetView extends React.PureComponent {
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
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});
const newState = {
token: parsed.params.token,
scheme: parsed.params.scheme,
code: parsed.params.code || ''
}
this.savedCred = parsed.params.cred;
if (this.props.reqCredMethod && parsed.params.cred) {
newState[this.props.reqCredMethod] = this.savedCred;
}
this.setState(newState);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.reqCredMethod != this.props.reqCredMethod && this.props.reqCredMethod) {
this.setState({[this.props.reqCredMethod]: this.savedCred || ''});
}
}
handleSubmit(e) {
e.preventDefault();
if (this.state.token) {
this.props.onReset(this.state.scheme, this.state.password.trim(), this.state.token);
} else if (this.state.sent) {
this.props.onCancel();
} else {
const email = this.state.email.trim();
this.setState({email: email});
this.props.onRequest('email', email).then(() => {
this.setState({sent: true});
// Reset using token.
this.props.onReset(this.state.password.trim(), {
scheme: 'token',
secret: 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.props.reqCredMethod == 'email' ? 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');
}
});
}
}
@@ -49,60 +108,124 @@ export default class PasswordResetView extends React.PureComponent {
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.code && this.props.reqCredMethod)) && 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.token && this.state.scheme) ?
<FormattedMessage id="new_password_placeholder" defaultMessage="Enter new password"
description="Placeholder for entering new password">{
(placeholder) => <VisiblePassword
placeholder={placeholder}
autoComplete="new-password"
value={this.state.password}
required={true} autoFocus={true}
onChange={this.handlePasswordChange} />
}</FormattedMessage>
: this.state.sent ?
<>
<br/>
<center><i className="material-icons huge green">task_alt</i></center>
<br/>
<center><FormattedMessage id="password_reset_email_sent"
defaultMessage="An email has been sent to {email}. Follow the directions in the email to reset your password."
values={{ email: <tt>{this.state.email}</tt> }}
description="Notification that the email with password reset instructions has been sent" /></center>
</>
:
<>
<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>
</>
}
{!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 || this.state.token || this.state.code ? 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">{
(this.state.token && this.state.scheme) ?
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" />
: this.state.sent ?
<FormattedMessage id="button_ok" defaultMessage="OK"
description="Button [OK]" />
showPasswordInput ?
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" />
:
<FormattedMessage id="button_send_request" defaultMessage="Send request"
description="Button [Send request]" />
<FormattedMessage id="button_send_request" defaultMessage="Send request"
description="Button [Send request]" />
}</button>
</div>
</form>
);
}
}
export default injectIntl(PasswordResetView);
+19 -4
View File
@@ -2,6 +2,8 @@
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 {
@@ -11,18 +13,21 @@ export default class SettingsView extends React.PureComponent {
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
serverAddress: this.state.serverAddress,
secureConnection: this.state.secureConnection,
});
}
@@ -34,10 +39,14 @@ export default class SettingsView extends React.PureComponent {
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'].map((item) => {
['def', 'ws', 'lp'].forEach(item => {
const id = 'transport-' + item;
const name = names[item];
transportOptions.push(
@@ -58,8 +67,14 @@ export default class SettingsView extends React.PureComponent {
description="Label for server selector in SettingsView" />
</label>
</div>
<HostSelector serverAddress={this.state.serverAddress}
onServerAddressChange={this.handleServerAddressChange} />
<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:"
+79 -41
View File
@@ -1,20 +1,21 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
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 TopicCommon from '../widgets/topic-common.jsx';
import TopicCommonView from './topic-common-view.jsx';
import AccountSettingsView from './account-settings-view.jsx';
const AccountSettingsView = React.lazy(_ => import('./account-settings-view.jsx'));
import ContactsView from './contacts-view.jsx';
import CreateAccountView from './create-account-view.jsx';
const CreateAccountView = React.lazy(_ => import('./create-account-view.jsx'));
import AccGeneralView from './acc-general-view.jsx';
import AccNotificationsView from './acc-notifications-view.jsx';
import AccSecurityView from './acc-security-view.jsx';
const AccSecurityView = React.lazy(_ => import('./acc-security-view.jsx'));
import AccSupportView from './acc-support-view.jsx';
import LoginView from './login-view.jsx';
import NewTopicView from './new-topic-view.jsx';
import PasswordResetView from './password-reset-view.jsx';
const PasswordResetView = React.lazy(_ => import('./password-reset-view.jsx'));
import SettingsView from './settings-view.jsx';
import ValidationView from './validation-view.jsx';
@@ -60,6 +61,11 @@ const messages = defineMessages({
description: 'Sidepanel title for AccNotificationsView.',
defaultMessage: 'Notifications'
},
acc_general: {
id: 'sidepanel_title_acc_general',
description: 'Sidepanel title for AccGeneralView.',
defaultMessage: 'General'
},
support: {
id: 'sidepanel_title_acc_support',
description: 'Sidepanel title for AccSupportView.',
@@ -92,7 +98,7 @@ const messages = defineMessages({
}
});
class SidepanelView extends React.Component {
class SidepanelView extends React.PureComponent {
constructor(props) {
super(props);
@@ -124,7 +130,7 @@ class SidepanelView extends React.Component {
}
return (
<div id="sidepanel" className={this.props.hideSelf ? 'nodisplay' : null}>
<div id="sidepanel">
<SideNavbar
state={view}
title={title}
@@ -155,57 +161,82 @@ class SidepanelView extends React.Component {
onPersistenceChange={this.props.onPersistenceChange} /> :
view === 'register' ?
<CreateAccountView
tinode={this.props.tinode}
onCreateAccount={this.props.onCreateAccount}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
<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' ?
<AccountSettingsView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
trustedBadges={this.props.trustedBadges}
onNavigate={this.props.onNavigate} /> :
<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}
reqCredMethod={this.props.reqCredMethod}
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') ?
<TopicCommon
<TopicCommonView
topic="me"
tinode={this.props.tinode}
myUserId={this.props.myUserId}
reqCredMethod={this.props.reqCredMethod}
onUpdateTopicDesc={this.props.onUpdateAccountDesc}
onUpdateTags={this.props.onUpdateAccountTags}
onCredAdd={this.props.onCredAdd}
onCredDelete={this.props.onCredDelete}
onCredConfirm={this.props.onCredConfirm}
onUpdateTagsRequest={this.props.onUpdateAccountTags}
onError={this.props.onError} /> :
view === 'acc_general' ?
<AccGeneralView
colorSchema={this.props.colorSchema}
textSize={this.props.textSize}
sendOnEnter={this.props.sendOnEnter}
onChangeColorSchema={this.props.onChangeColorSchema}
onTextSizeChanged={this.props.onTextSizeChanged}
onSendOnEnterChanged={this.props.onSendOnEnterChanged} /> :
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} /> :
onToggleMessageSounds={this.props.onToggleMessageSounds} /> :
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} /> :
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<AccSecurityView
tinode={this.props.tinode}
incognitoMode={this.props.incognitoMode}
onUpdateAccountDesc={this.props.onUpdateAccountDesc}
onUpdatePassword={this.props.onUpdatePassword}
onLogout={this.props.onLogout}
onDeleteAccount={this.props.onDeleteAccount}
onShowAlert={this.props.onShowAlert}
onShowBlocked={this.props.onShowBlocked}
onToggleIncognitoMode={this.props.onToggleIncognitoMode} />
</Suspense> :
view === 'support' ?
<AccSupportView
@@ -238,15 +269,22 @@ class SidepanelView extends React.Component {
<ValidationView
credCode={this.props.credCode}
credMethod={this.props.credMethod}
credToken={this.props.credToken}
onSubmit={this.props.onValidateCredentials}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
onCancel={this.props.onCancel} /> :
view === 'reset' ?
<PasswordResetView
onRequest={this.props.onPasswordResetRequest}
onReset={this.props.onResetPassword}
onCancel={this.props.onCancel} /> :
<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>
);
+957 -516
View File
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>
);
}
};
+42 -16
View File
@@ -20,17 +20,44 @@ class ValidationView extends React.PureComponent {
super(props);
this.state = {
code: props.credCode || ''
code: props.credCode || '',
codeReceived: props.credCode
};
this.handleChange = this.handleChange.bind(this);
this.handleCodeChange = this.handleCodeChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleChange(e) {
this.setState({code: e.target.value});
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) {
@@ -44,7 +71,7 @@ class ValidationView extends React.PureComponent {
handleSubmit(e) {
e.preventDefault();
if (this.state.code && this.state.code.trim()) {
this.props.onSubmit(this.props.credMethod, this.state.code.trim());
this.props.onSubmit(this.props.credMethod, this.state.code.trim(), this.props.credToken);
}
}
@@ -53,29 +80,28 @@ class ValidationView extends React.PureComponent {
this.props.onCancel();
}
render() {
const { formatMessage } = this.props.intl;
const methods = {'email': formatMessage(messages.email), 'tel': formatMessage(messages.phone)};
let method = methods[this.props.credMethod] || this.props.credMethod;
const method = methods[this.props.credMethod] || this.props.credMethod;
return (
<div className="panel-form">
<div className="panel-form-row">
<label className="small" htmlFor="enter-confirmation-code">
<label className="small gray" htmlFor="enter-confirmation-code">
<FormattedMessage id="enter_confirmation_code_prompt"
defaultMessage="Enter confirmation code sent to you by {method}:"
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.handleChange}
onKeyPress={this.handleKeyPress} required />
}</FormattedMessage>
<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}
onKeyDown={this.handleKeyPress} required />
}</FormattedMessage>
</div>
<div className="dialog-buttons">
<button className="secondary" onClick={this.handleCancel}>
+17 -16
View File
@@ -3,26 +3,27 @@ import { FormattedMessage } from 'react-intl';
export default class Alert extends React.PureComponent {
render() {
return this.props.visible ?
<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
}
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>
</div>
:
null
}
</div>
</div>
:
null;
</div>);
}
};
+5 -9
View File
@@ -27,15 +27,11 @@ export default class Attachment extends React.Component {
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) => {
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');
}
+272
View File
@@ -0,0 +1,272 @@
// 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.onloadedmetadata = 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.onloadedmetadata = _ => 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 = Math.max(this.viewBuffer[i] * height * 0.9, 1);
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 + 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 if (this.audioPlayer.readyState >= 2) {
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);
+419
View File
@@ -0,0 +1,419 @@
// 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;
// Default recording format (FF, Chrome except on iOS).
const DEFAULT_AUDIO_MIME_TYPE = 'audio/webm';
// Safari supports only mp4 as audio recording format.
const SAFARI_AUDIO_MIME_TYPE = 'audio/mp4';
const AUDIO_MIME_TYPES = [DEFAULT_AUDIO_MIME_TYPE, SAFARI_AUDIO_MIME_TYPE, ''];
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'
},
failed_to_init_audio: {
id: 'failed_to_init_audio',
defaultMessage: 'Failed to initialize audio recording',
description: 'Error message when audio is not available'
}
});
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.max(Math.min(this.viewBuffer[i] / BAR_SCALE, 0.9) * height, 1);
this.canvasContext.moveTo(x, (height - y) * 0.5);
this.canvasContext.lineTo(x, (height + 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;
AUDIO_MIME_TYPES.some(mimeType => {
if (MediaRecorder.isTypeSupported(mimeType)) {
this.mediaRecorder = new MediaRecorder(stream, {mimeType: mimeType, audioBitsPerSecond: 24_000});
return true;
}
return false;
});
if (!this.mediaRecorder) {
console.warn('MediaRecorder failed to initialize: no supported audio formats');
this.props.onError(this.props.intl.formatMessage(messages.failed_to_init_audio));
return;
}
// The following code is needed for visualization.
this.audioContext = new AudioContext();
this.audioInput = this.audioContext.createMediaStreamSource(stream);
if (!this.audioInput) {
console.warn('createMediaStreamSource returned null: audio input unavailable');
this.props.onError(this.props.intl.formatMessage(messages.failed_to_init_audio));
return;
}
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.
getRecording(mimeType) {
mimeType = mimeType || DEFAULT_AUDIO_MIME_TYPE;
let blob = new Blob(this.audioChunks, {type: mimeType});
// If duration is valid, apply fix for Chrome's WebM duration bug.
const result = mimeType == DEFAULT_AUDIO_MIME_TYPE ?
fixWebmDuration(blob, mimeType) :
Promise.resolve(blob);
return result
.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() {
if (this.audioInput) {
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);
+3 -3
View File
@@ -3,7 +3,7 @@ import React from 'react';
import LetterTile from './letter-tile.jsx';
import LoadSpinner from './load-spinner.jsx';
import { sanitizeImageUrl } from '../lib/utils.js';
import { sanitizeUrlForMime } from '../lib/utils.js';
import { AVATAR_SIZE } from '../config.js';
@@ -34,7 +34,7 @@ export default class AvatarUpload extends React.Component {
render() {
// Randomize id value in case more than one AvatarUpload is shown
// at the same time.
const randId = 'file-input-avatar-' + (Math.random() + '').substr(2);
const randId = 'file-input-avatar-' + ('' + Math.random()).substring(0, 4);
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
return (
<div className={className}>
@@ -44,7 +44,7 @@ export default class AvatarUpload extends React.Component {
<i className="material-icons">clear</i>
</a>}
{this.state.source ?
<img src={this.props.tinode.authorizeURL(sanitizeImageUrl(this.state.source))} className="preview" /> :
<img src={this.props.tinode.authorizeURL(sanitizeUrlForMime(this.state.source, 'image'))} className="preview" /> :
this.props.readOnly && this.props.uid ?
<div className="avatar-box">
<LetterTile
+2 -2
View File
@@ -29,8 +29,8 @@ class BadgeList extends React.PureComponent {
let badges = null;
if (this.props.trustedBadges && this.props.trustedBadges.length > 0) {
badges = [];
this.props.trustedBadges.map((b) => {
const comment = formatMessage(messages['badge_' + b]);
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>);
});
+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 { truncateString } from '../lib/strformat.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: truncateString(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">{truncateString(this.state.fullName, MAX_PEER_TITLE_LENGTH)}
<BadgeList short={true} trustedBadges={this.state.trustedBadges} /></div>
</div>
<div id="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');
const title = 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" />;
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>{title}</div>
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
</div>
</div>;
}
}
+713
View File
@@ -0,0 +1,713 @@
// 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 { truncateString } from '../lib/strformat.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.containerRef = React.createRef();
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);
this.bounds = {};
this.dragStart = (event) => {
if (!event.target.classList.contains('draggable')) {
// Prevent dragging when clicking on buttons or other non-draggable elements.
return;
}
if (window.matchMedia('(width<=640px)')) {
// No dragging on small screens.
return;
}
if (this.props.minimized && this.containerRef) {
event.stopPropagation();
this.containerRef.current.setPointerCapture(event.pointerId);
this.containerRef.current.style.cursor = 'grabbing';
this.bounds.left = this.containerRef.current.offsetLeft;
this.bounds.top = this.containerRef.current.offsetTop;
this.bounds.width = this.containerRef.current.offsetWidth;
this.bounds.height = this.containerRef.current.offsetHeight;
const parent = this.containerRef.current.offsetParent;
this.bounds.maxX = parent.offsetWidth;
this.bounds.maxY = parent.offsetHeight;
}
};
this.drag = (event) => {
event.stopPropagation();
const el = this.containerRef.current;
if (this.props.minimized && this.containerRef && el.hasPointerCapture(event.pointerId)) {
// Calculate new position. Restrict to the visible area of the parent element.
this.bounds.left = Math.min(Math.max(this.bounds.left + event.movementX, 0),
this.bounds.maxX - this.bounds.width);
this.bounds.top = Math.min(Math.max(this.bounds.top + event.movementY, 0),
this.bounds.maxY - this.bounds.height);
el.style.left = `${this.bounds.left}px`;
el.style.top = `${this.bounds.top}px`;
}
};
this.dragEnd = (event) => {
event.stopPropagation();
if (this.props.minimized && this.containerRef) {
this.containerRef.current.releasePointerCapture(event.pointerId);
this.containerRef.current.style.cursor = 'grab';
}
};
}
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();
}
if (this.containerRef) {
this.containerRef.current.addEventListener('pointerdown', this.dragStart);
this.containerRef.current.addEventListener('pointermove', this.drag);
this.containerRef.current.addEventListener('pointerup', this.dragEnd);
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onInfo = this.previousOnInfo;
this.stop();
if (this.containerRef) {
this.containerRef.current.removeEventListener('pointerdown', this.dragStart);
this.containerRef.current.removeEventListener('pointermove', this.drag);
this.containerRef.current.removeEventListener('pointerup', this.dragEnd);
}
}
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 = truncateString(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';
}
}
const minimizedClass = this.props.minimized ? 'minimized' : null;
const fullScreen = this.props.minimized ? 'fullscreen' : 'fullscreen_exit';
return (
<div id="video-container" className={minimizedClass} ref={this.containerRef}>
<div id="video-container-panel">
<div className="call-party self" disabled={this.state.audioOnly || this.props.minimized}>
<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} draggable`}>
<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>
<button className="full-screen" onClick={this.props.onToggleMinimize}>
<i className="material-icons">{fullScreen}</i>
</button>
<div id="controls" className={minimizedClass}>
<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);
+48
View File
@@ -0,0 +1,48 @@
// 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.
// callState: 'busy' | 'declined' | 'disconnected' | 'missed'.
// duration: <number> (call duration in milliseconds).
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}
</>
);
}
};
+73 -33
View File
@@ -1,14 +1,15 @@
// Single message, sent or received.
// Single chat message bubble, sent or received.
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk';
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, quoteFormatter } from '../lib/formatters.js';
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
import { fullFormatter } from '../lib/formatters.js';
import { sanitizeUrl } from '../lib/utils.js';
class BaseChatMessage extends React.PureComponent {
constructor(props) {
@@ -22,28 +23,30 @@ class BaseChatMessage extends React.PureComponent {
props.uploader.onProgress = this.handleProgress.bind(this);
}
this.handleImagePreview = this.handleImagePreview.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 = {
getFormatter: (tp) => { return tp == 'QQ' ? quoteFormatter : null; },
formatMessage: props.intl.formatMessage.bind(props.intl),
viewportWidth: props.viewportWidth,
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
onImagePreview: this.handleImagePreview,
onImagePreview: this.handleExpandImage,
onVideoPreview: this.handlePlayVideo,
onFormButtonClick: this.handleFormButtonClick,
onQuoteClick: this.handleQuoteClick
};
}
handleImagePreview(e) {
handleExpandImage(e) {
e.preventDefault();
this.props.onImagePreview({
this.props.onExpandMedia({
url: e.target.src,
filename: e.target.title,
filename: e.target.dataset.name,
width: e.target.dataset.width,
height: e.target.dataset.height,
size: e.target.dataset.size,
@@ -51,6 +54,21 @@ class BaseChatMessage extends React.PureComponent {
});
}
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 = {
@@ -75,22 +93,43 @@ class BaseChatMessage extends React.PureComponent {
if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
menuItems.push('menu_item_send_retry');
}
if (this.props.userIsWriter &&
this.props.received > Tinode.MESSAGE_STATUS_FAILED &&
this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) {
menuItems.push('menu_item_reply');
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,
forwarded: this.props.forwarded,
y: e.pageY,
x: e.pageX,
pickReply: this.props.pickReply
pickReply: this.props.pickReply,
editMessage: this.props.editMessage,
timestamp: this.props.timestamp,
}, menuItems);
}
@@ -112,19 +151,19 @@ class BaseChatMessage extends React.PureComponent {
}
render() {
const sideClass = this.props.deleted ? 'center' :
(this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'));
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.deleted ? null : (this.props.userAvatar || true);
const fullDisplay = (this.props.isGroup && this.props.response && !this.props.deleted &&
const avatar = this.props.userAvatar || true;
let textSizeClass = 'message-content';
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.
if (Drafty.isFormResponseType(att.mime)) {
// Don't show json drafty form response objects as attachments.
// They are not meant for users.
return;
}
@@ -142,13 +181,14 @@ class BaseChatMessage extends React.PureComponent {
}, this);
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
content = React.createElement(React.Fragment, null, tree);
} else if (this.props.deleted) {
// Message represents a range of deleted messages.
content = <><i className="material-icons gray">block</i> <i className="gray">
<FormattedMessage id="deleted_content"
defaultMessage="content deleted" description="Shown when messages are deleted" />
</i></>
} else if (typeof content != 'string') {
} else if (typeof content == 'string') {
// Make font bigger for emoji-only messages.
// Must use 'new RegExp(...)' because React does not like /{1-5}/.
if (new RegExp('^\\p{RGI_Emoji}{1,5}$', 'v').test(content || '')) {
// Content consists of 1-5 emoji characters. Count how many and use it to increase the font size.
textSizeClass += ' emoji-' + (content || '').match(/\p{RGI_Emoji}/vg).length;
}
} else {
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></>
@@ -170,23 +210,23 @@ class BaseChatMessage extends React.PureComponent {
<div>
<div className={bubbleClass}>
<div className="content-meta">
<div className="message-content">
<div className={textSizeClass}>
{content}
{attachments}
</div>
{this.props.timestamp ?
<ReceivedMarker
edited={this.props.edited}
timestamp={this.props.timestamp}
received={this.props.received} />
: null}
</div>
{this.props.deleted || !this.props.showContextMenu ?
null :
{this.props.showContextMenu ?
<span className="menuTrigger">
<a href="#" onClick={this.handleContextClick}>
<i className="material-icons">expand_more</i>
</a>
</span>
</span> : null
}
</div>
{fullDisplay ?
+34 -15
View File
@@ -1,6 +1,6 @@
import React from 'react';
/* BEGIN CheckBox: styled checkbox */
/* CheckBox: styled three-state checkbox, either clickable or static */
export default class CheckBox extends React.PureComponent {
constructor(props) {
super(props);
@@ -13,19 +13,38 @@ export default class CheckBox extends React.PureComponent {
}
render() {
return (
this.props.onChange ? (
this.props.checked === true ?
<i className="material-icons blue clickable" onClick={this.handleChange}>check_box</i> :
this.props.checked === false ?
<i className="material-icons blue clickable" onClick={this.handleChange}>check_box_outline_blank</i> :
<i className="material-icons lt-blue">indeterminate_check_box</i>
) : (
this.props.checked ?
<i className="material-icons">check_box</i> :
<i className="material-icons">check_box_outline_blank</i>
)
);
let classList = ['material-icons'];
let iconName;
if (Array.isArray(this.props.className)) {
classList.push(...this.props.className);
} else if (this.props.className) {
classList.push(this.props.className);
}
if (this.props.onChange) {
if (this.props.checked) {
classList.push('blue', 'clickable');
iconName = 'check_box';
} else if (this.props.checked === false) {
classList.push('blue', 'clickable');
iconName = 'check_box_outline_blank';
} else {
classList.push('lt-blue');
iconName = 'indeterminate_check_box';
}
} else {
if (this.props.checked) {
iconName = 'check_box';
} else {
iconName = 'check_box_outline_blank';
}
}
let attrs = {
className: classList.join(' '),
id: this.props.id,
};
if (this.props.onChange) {
attrs.onClick = this.handleChange;
}
return React.createElement('i', attrs, iconName);
}
}
/* END CheckBox */
+3 -3
View File
@@ -44,7 +44,7 @@ export default class ChipInput extends React.Component {
static indexChips(chips) {
const index = {};
let count = 0;
chips.map((item) => {
chips.forEach(item => {
index[item.user] = count;
count ++;
});
@@ -55,7 +55,7 @@ export default class ChipInput extends React.Component {
static sortChips(chips, keep) {
const required = [];
const normal = [];
chips.map((item) => {
chips.forEach(item => {
if (keep && keep.includes(item.user)) {
required.push(item);
} else {
@@ -115,7 +115,7 @@ export default class ChipInput extends React.Component {
const chips = [];
let count = 0;
const staticMembers = this.props.staticMembers || [];
this.state.sortedChips.map((item) => {
this.state.sortedChips.forEach(item => {
chips.push(
<Chip
tinode={this.props.tinode}

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