Compare commits

...

551 Commits

Author SHA1 Message Date
Nicolas Werner 1f68bc60b5 Actually forward keys using m.forwared_room_key 4 years ago
Nicolas Werner c25fd947a8 Automatic key sharing with trusted users 4 years ago
Nicolas Werner bd0deb7213 Fix crash on exit 4 years ago
Nicolas Werner bc7494473b Answer key requests for all our sessions 4 years ago
DeepBlueV7.X b23913fa7c
Merge pull request #301 from kamathmanu/issue266-filenameOnHover 4 years ago
kamathmanu e1c4f7d516 Make Text element a sibling of the Rectangle to avoid the text from depending on the rectangle's opacity property. Switch to Nheko theme colors 4 years ago
kamathmanu e3c6656613 Avoid hardcoded font size 4 years ago
Weblate 70b5e1e08e Translated using Weblate (Estonian) 4 years ago
kamathmanu a0c2a174ea Display filename on mouse hover 4 years ago
Nicolas Werner 62d0bdbb77 Fix small typo in robustness logic 4 years ago
Nicolas Werner d075a90024 Fix emoji filter losing focus 4 years ago
Nicolas Werner 716992b761 Update translations 4 years ago
Joseph Donofry 3499abd99a
Merge branch 'master' of ssh://github.com/Nheko-Reborn/nheko 4 years ago
Joseph Donofry 31c6857f19
Add some try...catch logic around read-only databases so nheko won't crash if the db doesn't exist 4 years ago
DeepBlueV7.X 82374791c4
Merge pull request #298 from LorenDB/master 4 years ago
DeepBlueV7.X a9c0684a5a
Merge pull request #299 from Nheko-Reborn/qml-linting 4 years ago
Nicolas Werner 1a029112d9 Lint qml with qml-format 4 years ago
DeepBlueV7.X 517a126a44
Merge pull request #270 from Chethan2k1/device-verification 4 years ago
Nicolas Werner 392d7d5568 Try to fix windows build 4 years ago
Nicolas Werner 57a6c05eab More unused variables in bindings 4 years ago
Nicolas Werner 684cfacfad Bump spdlog version 4 years ago
Nicolas Werner 56ba7de501 Fix unused variable warnings on old compilers 4 years ago
Nicolas Werner 8ec76daeda Send master key in verification flow (if we trust it) 4 years ago
Nicolas Werner e5fb9a25ea Try to please the CI gods 4 years ago
Nicolas Werner 08d5a84cbd Fix issues with old qt and bump to 5.10 4 years ago
Nicolas Werner 99ba1f17d3 Merge remote-tracking branch 'origin/master' into cross-signing 4 years ago
Nicolas Werner cd43147b77 Implement signature upload for own master key 4 years ago
Nicolas Werner 7b6fab3373 Calculate verification status from cross-signing sigs and update dynamically 4 years ago
LorenDB d7c3fa844e Begin Esperanto translations 4 years ago
Nicolas Werner 64d5a193f1 Fix in room verification 4 years ago
Nicolas Werner 8a4d85f801 Show different verification errors 4 years ago
Nicolas Werner bca29a4227 Make steps in verification flow explicit 4 years ago
Nicolas Werner 2a79cd2b6b Don't fail on missing key for a device and /rotate-megolm-session command 4 years ago
Nicolas Werner abff61bb6c Fix nullopt when iterating reactions 4 years ago
Nicolas Werner 51964c4fd7 Clean up verification pages a bit 4 years ago
Nicolas Werner d4dccc8e36 Fix room name 4 years ago
Nicolas Werner 600df6d2ec Fix fetching inexistent batch tokens 4 years ago
Nicolas Werner c9de044e32 Handle forwarded room keys 4 years ago
Nicolas Werner 54e3c2c96b Fix verification start to start with request 4 years ago
DeepBlueV7.X 8110f22222
Merge pull request #294 from trilene/master 4 years ago
Nicolas Werner 94690ebd4c Clean up verification and key cache a bit 4 years ago
trilene 28e9a7ad40 Bump mtxclient 4 years ago
trilene 4caa206483 Bump mtxclient 4 years ago
Weblate 1a97859930 Added translation using Weblate (Portuguese (Portugal)) 4 years ago
DeepBlueV7.X e5cb8c08ba
Merge pull request #290 from trilene/master 4 years ago
trilene 5bfe0cd178 Remove duplicate control from settings page 4 years ago
DeepBlueV7.X 5cce5b9999
Merge pull request #289 from trilene/master 4 years ago
trilene e57199412a Allow button colors override 4 years ago
trilene aab6cb88a5 Fix build for those without GStreamer 4 years ago
trilene 3f73853e4b Move ActiveCallBar Qml to separate file 4 years ago
Nicolas Werner 4802c34009 Merge remote-tracking branch 'origin/master' into cross-signing 4 years ago
Weblate 5e0eb945ae Translated using Weblate (French) 4 years ago
Weblate 0cca0f17ee Translated using Weblate (French) 4 years ago
trilene 44cfc8d22a clang-format 4 years ago
trilene da27670cbe Port ActiveCallBar to Qml 4 years ago
Weblate 9169a26e67 Translated using Weblate (Estonian) 4 years ago
Nicolas Werner 54db9c89ed Simplify outbound session setup 4 years ago
Nicolas Werner 67302b3674 Fix some missing english translations 4 years ago
Nicolas Werner 4179be427f Fix binding loop when trying to reply 4 years ago
Weblate beef474582 Translated using Weblate (Estonian) 4 years ago
Weblate e59bf3564d Translated using Weblate (German) 4 years ago
Nicolas Werner 0afe1299e2 Fix emoji escape in file messages 4 years ago
Nicolas Werner 7b7d29737f Fix cut off english translation. 4 years ago
DeepBlueV7.X fe325f9266
Merge pull request #285 from Lurkki14/master 4 years ago
Weblate 6c63762aa4 Translated using Weblate (Estonian) 4 years ago
Weblate 0a7189b6a1 Translated using Weblate (English) 4 years ago
Weblate b4eb1c6df9 Translated using Weblate (English) 4 years ago
Weblate a253eda5fd Translated using Weblate (English) 4 years ago
Weblate 145e207848 Translated using Weblate (Estonian) 4 years ago
Weblate 78572d9d88 Translated using Weblate (English) 4 years ago
Weblate b2c3639f04 Translated using Weblate (German) 4 years ago
Nicolas Werner 4b36585e47 Fix typo in encryption error message 4 years ago
Weblate 1e0962d97c Translated using Weblate (German) 4 years ago
Nicolas Werner 75d7c2608a Update translations 4 years ago
Weblate f10970cc83 Translated using Weblate (Estonian) 4 years ago
Weblate ea2f7283ca Translated using Weblate (Finnish) 4 years ago
Weblate 8cb45ef834 Translated using Weblate (German) 4 years ago
Weblate 5c5134cea2 Translated using Weblate (Estonian) 4 years ago
Jussi Kuokkanen 13658d536e make typing Rectangle extents cleaner 4 years ago
Jussi Kuokkanen bafe181010 Merge branch 'master' of https://github.com/Lurkki14/nheko 4 years ago
Jussi Kuokkanen 438dcd3c5e Merge branch 'master' of https://github.com/Nheko-Reborn/nheko 4 years ago
Lurkki14 8741e5f36a
Set typing Rectangle color directly instead of Connection 4 years ago
DeepBlueV7.X 791a01487b
Merge pull request #286 from trilene/voip 4 years ago
trilene e065bf2205 Remove GStreamer version test 4 years ago
trilene f7beb1b34e clang-format 4 years ago
trilene 8d7c70d912 Fix preprocessor directive 4 years ago
trilene b527c5a21c Adapt device monitoring for GStreamer 1.18 4 years ago
Jussi Kuokkanen 1f71f7227a make Rectangle to fill the gap between input widget and typing users 4 years ago
Jussi Kuokkanen c463568031 simplify check for no typing users 4 years ago
Jussi Kuokkanen 7d72356318 Merge branch 'master' of https://github.com/Nheko-Reborn/nheko 4 years ago
Jussi Kuokkanen e49691fe3b use Rectangle for typing users so it doesn't overlap with the timeline 4 years ago
DeepBlueV7.X c62db00e6f
Merge pull request #284 from Nheko-Reborn/hidden-events 4 years ago
Nicolas Werner c5f93efcd3 Actually use room account data 4 years ago
Nicolas Werner 8af056faa9 Fix avatars in notifications 4 years ago
Nicolas Werner fef0cc2d71 Fix some images not showing up 4 years ago
Nicolas Werner c2f2e8324c Update blurhash 4 years ago
Nicolas Werner b05c101021 Fix null errors in qml 4 years ago
Nicolas Werner 77e241b9e5 Reenable top bar room settings menus on avatar or title clicks 4 years ago
Nicolas Werner 640b0ee405 Port top bar to Qml 4 years ago
DeepBlueV7.X 66d6307252
Merge pull request #283 from trilene/voip 4 years ago
trilene 2526a5604e Remove bus watch when call ends 4 years ago
Weblate 9b8e696979 Translated using Weblate (Estonian) 4 years ago
Weblate 83f0e2772c Translated using Weblate (Estonian) 4 years ago
Weblate 25b5a21ecd Merge branch 'master' of github.com:Nheko-Reborn/nheko 4 years ago
Weblate be2c4e5021 Added translation using Weblate (Estonian) 4 years ago
Weblate ff8cec1ea3 Update translation files 4 years ago
DeepBlueV7.X d02e899941
Merge pull request #281 from trilene/voip 4 years ago
trilene bd0e66b548 Don't show default audio source in settings 4 years ago
CH Chethan Reddy 8eb74daf76 Split qml part of Device Verification 4 years ago
Weblate b5669310e5 Merge branch 'master' of github.com:Nheko-Reborn/nheko 4 years ago
Weblate a09a37cc3c Add simpified chinese translations 4 years ago
Nicolas Werner 94e1b52ddd Single line click handler 4 years ago
Nicolas Werner b934cf329c Clean up UserProfile a bit 4 years ago
DeepBlueV7.X 75e646968d
Merge pull request #275 from Chethan2k1/master 4 years ago
DeepBlueV7.X a7b979084f
Merge pull request #280 from trilene/voip 4 years ago
trilene 124952a11c Ignore empty remote ICE candidates 4 years ago
DeepBlueV7.X d38a4dcf5d
Merge pull request #279 from Lurkki14/master 4 years ago
Jussi Kuokkanen bfcfa79d53 reset filter string when emoji completer is opened 4 years ago
Chethan2k1 f6a47ce72f Some fixes 4 years ago
DeepBlueV7.X b37534aa53
Merge pull request #278 from trilene/voip 4 years ago
trilene b6563d9ffe GStreamer v1.18.0 released 4 years ago
trilene 7d2844b2b0 Fix earlier commit when GStreamer < v1.17 4 years ago
Chethan2k1 2b5deabbdc Fix breaking while using qmlRegisterSingletonInstance 4 years ago
Chethan2k1 a27662dc08 Making sure Verification Objects are deleted properly 4 years ago
Chethan2k1 e70b4e4268 Fix wrong tran_id issue 4 years ago
Chethan2k1 898be090af Add support non-encrypted room-verification messages 4 years ago
Nicolas Werner c4e4938d35 Save account data and allow hiding events via account data 4 years ago
Chethan 0b03d40bf5
Update README.md 4 years ago
Chethan2k1 db0d10f38e Fix Readme 4 years ago
Chethan2k1 e8eeb480d5 Fix Wrong Emojis Issue in Room Verification 4 years ago
DeepBlueV7.X 657f4073e9
Merge branch 'master' into device-verification 4 years ago
CH Chethan Reddy 5358854de3 Add support for Encrypted to-device verification messages 4 years ago
Nicolas Werner 10f09d4f43 Fix catch by value warning 4 years ago
Nicolas Werner e0981e17a1 Fix reactions 4 years ago
Nicolas Werner 8d14a058c6 Fix endless pagination, when old history is inaccessible 4 years ago
DeepBlueV7.X eb554e8266
Merge pull request #267 from Lurkki14/master 4 years ago
Jussi Kuokkanen 8f872f1961 remove unused includes and use QRegularExpression 4 years ago
Jussi Kuokkanen bb4636885d remove comment 4 years ago
Jussi Kuokkanen beec2607fc get completion string based on trigger position instead of current word 4 years ago
Jussi Kuokkanen 254b7549eb ignore enter keypress when completion is selected 4 years ago
CH Chethan Reddy f03a48eec5 fix 4 years ago
CH Chethan Reddy b174bd9380 Merge remote-tracking branch 'upstream/master' into device-verification 4 years ago
CH Chethan Reddy 3396a7a796 Change the tag for mtxclient 4 years ago
CH Chethan Reddy 9a76db85d5 Change ReactionRealtesTo to RelatesTo 4 years ago
Jussi Kuokkanen aed8d23aca don't select emoji completion by default and add minimum string length before showing completions 4 years ago
CH Chethan Reddy 0d1dd29b19 Small Fixes 4 years ago
Jussi Kuokkanen f40d8d15b5 undo changes to emoji_codegen.py 4 years ago
Jussi Kuokkanen 9ad9c8ddf0 fix build and remove commented code 4 years ago
Jussi Kuokkanen 7acd4b3307 lint 4 years ago
Jussi Kuokkanen 5e344d2685 Merge branch 'master' of https://github.com/Nheko-Reborn/nheko 4 years ago
Jussi Kuokkanen a173d964f7 add emoji completer to text input 4 years ago
DeepBlueV7.X b7b9cee30e
Merge pull request #265 from trilene/voip 4 years ago
trilene 67a6ab401b Link GStreamer elements before syncing state 4 years ago
DeepBlueV7.X b58e370c03
Merge pull request #251 from Nheko-Reborn/new-event-store 4 years ago
Nicolas Werner 3df4bde032 Add some log messages, that migrations are in progress 4 years ago
Nicolas Werner 9f79b85579 Speedup db a bit, but loose some crash resiliency 4 years ago
CH Chethan Reddy 19cfd08a55 Verify signatures and find trusted devices 4 years ago
CH Chethan Reddy 1d299951b6 Cache Fix 4 years ago
DeepBlueV7.X 898297a7b0
Merge pull request #263 from trilene/voip 4 years ago
trilene 473293b6a5 Under GStreamer >= 1.17 gather all candidates before sending offer/answer 4 years ago
DeepBlueV7.X 706ba84d73
Merge pull request #262 from bqv/patch-1 4 years ago
Tony O d61d108a4f
Update README.md 4 years ago
CH Chethan Reddy 8a4bd37fea [WIP] Room Verification Works! 4 years ago
DeepBlueV7.X b5d406ad80
Merge pull request #259 from trilene/voip 4 years ago
Nicolas Werner d6bc05fcd6 Bump mtxclient 4 years ago
trilene 1402732b5f Stop SendFile and Call buttons swapping places on file upload 4 years ago
Nicolas Werner 7f7108161e Hide CallCandidates again in new store 4 years ago
Nicolas Werner de7ec4d2b3 Merge remote-tracking branch 'origin/master' into new-event-store 4 years ago
Mihai Fufezan 7c1ca38d98 Translated using Weblate (English) 4 years ago
Mihai Fufezan ed17b0c33b Added translation using Weblate (Romanian) 4 years ago
DeepBlueV7.X 3fece53eb7
Merge pull request #237 from trilene/voip 4 years ago
Nicolas Werner b6751ab01e Appease the linter 4 years ago
Nicolas Werner e06ff1ac1f Fix SVG color undefined 4 years ago
Nicolas Werner f157602a52 Disable call support, when GStreamer is unavailable 4 years ago
Nicolas Werner 29cb065102 Bump flatpak dependency 4 years ago
Nicolas Werner 14a0aac748 Add /clear-timeline command 4 years ago
CH Chethan Reddy 2e20049b36 [WIP] Room-Verification Messages 4 years ago
Nicolas Werner 1e9efa3072 Try to fix variable timestamp width 4 years ago
Nicolas Werner b972d827cb Try to fix issue of pagination interfering with limited: true 4 years ago
Nicolas Werner 7eb0c4e09c Also request keys from own devices 4 years ago
Nicolas Werner 1f9215a5be Split error messages from event decryption 4 years ago
Nicolas Werner dbaddb0165 Further tweak text element 4 years ago
trilene df65093374 Add audio input device selector 4 years ago
trilene 02dfc8039f Conditionally compile against upcoming GStreamer release 4 years ago
trilene b86711a388 Merge remote-tracking branch 'upstream/master' into voip 4 years ago
trilene 979bba6460 Tweak AcceptCall dialog 4 years ago
trilene e527da052b Reset TLVM initial sync flag on logout 4 years ago
trilene e3e7595bab clang-format 4 years ago
trilene f14d141cb5 Improve TURN server retrieval 4 years ago
trilene c0743f9688 Fix error message 4 years ago
HelaBasa 48877307ca Added translation using Weblate (Sinhala) 4 years ago
CH Chethan Reddy 3635c185e9 Add Room Verification Messages 4 years ago
CH Chethan Reddy 1fcd768f88 Adding Room Key Verification Stuff 4 years ago
CH Chethan Reddy a2979c2df1 Updating keys of outdated encrypted users 4 years ago
CH Chethan Reddy 1103cc15cf Adding icons to UserProfile 4 years ago
Nicolas Werner 08028d5c57 Refactor UserProfile 4 years ago
CH Chethan Reddy ac1fbbb69f Some issue with UserProfile 4 years ago
CH Chethan Reddy 6fae36abc4 [WIP] Add Caching for users 4 years ago
CH Chethan Reddy ffa61095b8 Error Handling and some fixes 4 years ago
CH Chethan Reddy 00e36b6068 Add some Userprofile buttons 4 years ago
CH Chethan Reddy 75efa5d3a2 Fix the Weird auto-confirmation and cancellation 4 years ago
CH Chethan Reddy fd232b1f4a Some more fixes 4 years ago
Nicolas Werner 4862be06be Fix presence indicator 4 years ago
CH Chethan Reddy 1633650303 Some more changes 4 years ago
CH Chethan Reddy d49ab15656 Some Improvements 4 years ago
CH Chethan Reddy ce013e67a6 Add some more slots and mac 4 years ago
CH Chethan Reddy 67367d0004 Shared secret with decimal and emoji works! 4 years ago
CH Chethan Reddy 41b6ef0c32 Add DeviceVerificationList to keep track of all flows and Popup on 4 years ago
Chethan2k1 1eb162cb6f Handle Device Verification related to_device messages 4 years ago
Chethan2k1 cd5dd0e39b Add SAS Method choice and Add send_to_device API call 4 years ago
Chethan2k1 b628f485ff Tweak UI for device verification and Add more slots 4 years ago
CH Chethan Reddy f9c0f4dd54 Add C++ Model for DeviceList 4 years ago
Chethan2k1 a54a973ad6 Adding DeviceList for userprofile 4 years ago
CH Chethan Reddy 64f204d984 Rewrite UserProfile in qml 4 years ago
Nicolas Werner 707248fea3 Add DeviceVerificationFlow dummy and verification test button 4 years ago
Nicolas Werner b1362ca69f Use label in device verification dialogs (for proper theming) 4 years ago
Nicolas Werner fed0463e57 Make emojis a bit smaller 4 years ago
Nicolas Werner 480c4bc8f5 Set proper emoji font for device verification 4 years ago
Nicolas Werner 2088053d26 Add DeviceVerificationFlow dummy and verification test button 4 years ago
Nicolas Werner 488cc5e73b First design iteration of device verification dialogs 4 years ago
Nicolas Werner 7f3d97517f Fix double free by closing cursor at the right time 4 years ago
trilene 97681ccf64 Remove references to video calls 4 years ago
Nicolas Werner 12090c0a06 Add workaround for duplicate syncs 4 years ago
Nicolas Werner 720bb164f7 Fix migration (hopefully) 4 years ago
trilene 43ec0c0624 Handle ICE failure 4 years ago
Nicolas Werner ade905c881 Fix shadowing variable 4 years ago
Nicolas Werner a00b11def7 Rename EventStore::event to get to remove ambiguity with QObject::event 4 years ago
Nicolas Werner 28e7ea40cb Bump mtxclient and use git dependency in flatpak 4 years ago
Nicolas Werner 8bf26917ad Make long press menu actually work 4 years ago
trilene 57d5a3d31f Improve debug messages 4 years ago
Nicolas Werner 6f557c19a1 Optimize scrolling a little bit 4 years ago
Nicolas Werner cbb4356b19 Fix more non integer heights 4 years ago
Nicolas Werner fdcf91f5eb Fix binding loop and non integer text height 4 years ago
Nicolas Werner 4e7bd20e0c Reset fetch in progress when fetch failed 4 years ago
Nicolas Werner b294430fe5 Return to redacted messages instead of just storing the redaction 4 years ago
trilene 6be21beebd Acknowledge source of ringtones 4 years ago
trilene aec24efbe2 Specify call type on timeline 4 years ago
trilene d51e34e66a Cancel sent CallInvite if no response within expiry 4 years ago
trilene a4301048e3 Fix QLayout warning 4 years ago
trilene d508e3abd6 Send ICE candidates gathered after timeout 4 years ago
trilene 7377215d28 Confirm logout/quit if active call in progress 4 years ago
trilene 55783c6fe5 Tweak ActiveCallBar 4 years ago
Nicolas Werner 147ae68c31 Don't send url, if we send an encrypted file 4 years ago
trilene 88cfa3a8fa Polish voice call UI 4 years ago
Nicolas Werner f23d733cff Fix room joins 4 years ago
trilene da9995fc3d Update mtxclient reference 4 years ago
Nicolas Werner 19f27236ea Fix reactions 4 years ago
DeepBlueV7.X 12cb77da9c
Merge pull request #247 from not-chicken/centerDate 4 years ago
Nicolas Werner 6f2bc908ba Fix reaction display 4 years ago
Lorem e86c1cc79f Make dateBubble's text vertically aligned to vertical center 4 years ago
Nicolas Werner d467568a65 Close cursor we don't need and where we overwrite the contents 4 years ago
Nicolas Werner 8261446f83 Fix reply scrolling 4 years ago
Nicolas Werner 5695f004a2 Fix race condition between /messages and /sync 4 years ago
Nicolas Werner 046b3f4da6 Mark own events as read again after sending 4 years ago
Nicolas Werner 36e4405f25 Fix flickering of encrypted messages when sending using new store 4 years ago
Nicolas Werner a5dda86a6c Fix encryption indicator 4 years ago
Nicolas Werner 56ea89aa11 Reenable sending messages 4 years ago
trilene 28a678ca60 Add comment wrt TURN server credentials 4 years ago
trilene f8ef55c133 Case-insensitive sdp search 4 years ago
Nicolas Werner aa34576dfd Warn before kicking,banning,inviting,etc 4 years ago
trilene 8968d51b65 Handle preemptive CallAnswer from fellow device 4 years ago
trilene 16209ce073 Hide incoming CallCandidates in encrypted rooms 4 years ago
trilene 195ba5e5ee Remove comments 4 years ago
trilene 9d6cce9fe3 Handle preemptive CallHangUp 4 years ago
trilene 774d864096 Hide CallCandidates events from the timeline 4 years ago
trilene 09d2d937c5 Centre PlaceCall dialog 4 years ago
trilene e85652e7e7 Fix percent-encoding of TURN server URI 4 years ago
Nicolas Werner 9ae7d0dce3 Readd pagination and fix redactions 4 years ago
Nicolas Werner 9479fcde08 Initialize Profile later 4 years ago
trilene c73cfe1810 Merge remote-tracking branch 'upstream/master' into voip 4 years ago
trilene 7a206441c8 Support voice calls 4 years ago
Nicolas Werner da2f80df60 Fix translation loading 4 years ago
Nicolas Werner 3421728898 Fetch missing events 4 years ago
Nicolas Werner 530c531c4b WIP: Event Store split out 4 years ago
Nicolas Werner 7650e6ced6 Fix autolinking breaking on single quotes in href attribute 4 years ago
Nicolas Werner 8d3ab300b6 Open url using QDesktopServices from Qml 4 years ago
Nicolas Werner fe12e63c7c Fix parent undefined warning 4 years ago
Nicolas Werner 0da1a6d5fc Add relations and order without hidden events to db 4 years ago
Nicolas Werner 82eff09062 Fetch event from db and use string_view where possible 4 years ago
Nicolas Werner c79205c26a Use new timeline cache structure 4 years ago
Nicolas Werner 79a29953dd Persist event order 4 years ago
Nicolas Werner 233b3c06ce Store events in room specific db 4 years ago
Nicolas Werner d72eb5eb2d Update dependencies 4 years ago
Nicolas Werner 21a1f249f9 Fix Qt 5.9 build 4 years ago
Nicolas Werner d177405913 Don't compile qml by default 4 years ago
Nicolas Werner f6fa494666 Make connections across threads queued in any case 4 years ago
Nicolas Werner da975038db Drop libsodium dependency 4 years ago
Nicolas Werner a9e321e38f Don't use formatted body without format 4 years ago
DeepBlueV7.X 09e9cffcae
Merge pull request #228 from felixonmars/patch-1 4 years ago
Felix Yan 751c0526d7
Correct a typo in SnackBar.cpp 4 years ago
Nicolas Werner c973fd759b Fix m.relates_to being sent as 'null' when not set in encrypted messages. 4 years ago
Nicolas Werner cded494cb5 Try to make text rendering in qml sharper 4 years ago
Nicolas Werner dde28c627b Lower qt version used for emoji picker and scroll helper 4 years ago
Nicolas Werner 5c8ee99e9a Remove online indicator for offline users 4 years ago
DeepBlueV7.X 887ad25ab7
Merge pull request #220 from not-chicken/theme 5 years ago
Lorem e1706f4df5 Fix not being able to change theme 5 years ago
Nicolas Werner 24d2a2d821 Update mtxclient version to tag 5 years ago
Joseph Donofry 95d5e63f4b
Version number fixes 5 years ago
Joseph Donofry 3ea5a92b2a
Prepare 0.7.2 release 5 years ago
Nicolas Werner a09039a3be Add presence and upgrade notes to the changelog 5 years ago
Joseph Donofry ef51b5e947
Merge pull request #219 from Nheko-Reborn/changelog 5 years ago
Joseph Donofry 21dfb3c0b9
Merge pull request #216 from Nheko-Reborn/presence 5 years ago
Joseph Donofry a96e6e5ecd
Merge branch origin/master and update translations 5 years ago
Joseph Donofry 7afb164244
Merge remote-tracking branch 'origin/master' into presence 5 years ago
Joseph Donofry 813884ee0c
Merge pull request #217 from Nheko-Reborn/reactions 5 years ago
Joseph Donofry dbaf92734c
Fix formatting 5 years ago
Joseph Donofry 34ed487c08
Update translations 5 years ago
Joseph Donofry b90b718961
Merge remote-tracking branch 'origin/master' into reactions 5 years ago
Joseph Donofry b9626f0c69
Additional code cleanup 5 years ago
Nicolas Werner 866f59f79c Update changelog 5 years ago
Nicolas Werner 150c9b1dbc Fix scale factor setting 5 years ago
Nicolas Werner db93e6b853 Fix warning about not registered Presence type 5 years ago
Nicolas Werner 488924c9b3 Allow deleting a status 5 years ago
Joseph Donofry 12c46e86b4
Fix linting 5 years ago
Joseph Donofry 5e355c36fd
Fix build issue on some versions of clang 5 years ago
DeepBlueV7.X 75bb037bb7
Merge pull request #218 from z33ky/alert-notifications 5 years ago
Alexander 'z33ky' Hirsch 2b9860c3af Add setting to alert on notification 5 years ago
Joseph Donofry 39b240e25a
Remove unused QML file 5 years ago
Joseph Donofry 73f4c26dbe
Update translations 5 years ago
Joseph Donofry 0078c72a37
Remove signals in favor of direct function calls 5 years ago
Joseph Donofry 75cdc1eee2
Remove duplicate search icons 5 years ago
Joseph Donofry 07ffd9e7e9
Fix UserSettings in QML 5 years ago
Joseph Donofry 6bb73f84a3
Merge master and fix conflicts 5 years ago
Joseph Donofry fa34749279
Minor adjustments to emoji picker 5 years ago
Nicolas Werner 0a23615dd7 Fix small formatting error 5 years ago
Nicolas Werner b9631753dd Reenable custom status messages 5 years ago
Nicolas Werner 3baf11b5c4 Fix unused capture 5 years ago
Nicolas Werner 814868024c Disable presence and status_msg menu 5 years ago
Nicolas Werner bf440f9a31 Bump mtxclient 5 years ago
Nicolas Werner f2bfa61e08 Add menu actions to set presence 5 years ago
Nicolas Werner 96f4169be9 Show presence and set custom status messages 5 years ago
Nicolas Werner e5a55ab1b9 Smooth scaling for images 5 years ago
Nicolas Werner 5e684a0a2f Download boost from sourceforge 5 years ago
Nicolas Werner c40429af23 Bump mtxclient version 5 years ago
Nicolas Werner f4b84327e8 Limit jobs on arm 5 years ago
Nicolas Werner 43d2ebc095 Fix Qt5.15 issues 5 years ago
Nicolas Werner f8903f493f Extend timeout 5 years ago
Nicolas Werner 95f29a3d19 Try travis_wait alternative 5 years ago
Nicolas Werner 5ca5b4561e Travis wait can only be used in a top level command? 5 years ago
Nicolas Werner 190b6cb3c7 Add travis_wait to flatpak builds 5 years ago
Nicolas Werner c8ba385cb9 Install dependencies for arm manually to prevent timeout 5 years ago
Nicolas Werner 50d5891493 Fix serialization bug introduced in 9eddcfc42f 5 years ago
Nicolas Werner 4d20839d74 Skip precompiled headers for obc-c code 5 years ago
Nicolas Werner 9eddcfc42f Remove some redundant functions 5 years ago
DeepBlueV7.X fd270dcd55 Translated using Weblate (Italian) 5 years ago
Nicolas Werner 6b60ff7713 Rename settings to be more consistent 5 years ago
Nicolas Werner f452bdf2b0 Make settings update immediately in qml and allow limiting timeline width 5 years ago
Nicolas Werner 4ee9e5c27c Improve compile times a tiny bit 5 years ago
Nicolas Werner 4e5bd53b13 Optionally use precompiled headers 5 years ago
Nicolas Werner fe45c49e56 Bump singleapplication version 5 years ago
Nicolas Werner 247539cb5a Set cxx standard as cache variable for when toolchain file is not used 5 years ago
Nicolas Werner 937b35ca8a Fix some join messages showing as empty 5 years ago
Lorenzo Ancora f7cd0c4137 Added translation using Weblate (Italian) 5 years ago
DeepBlueV7.X fbcc53a632
Merge pull request #205 from lkito/SettingTooltips 5 years ago
lkito 60ad6ce277 Added an option to have descriptions for user settings. 5 years ago
DeepBlueV7.X 33ba81a2e6
Merge pull request #204 from lkito/master 5 years ago
Joseph Donofry e20cddd005
Update translations 5 years ago
Joseph Donofry 5228861b88
Add reaction/redaction for in-line Reactions 5 years ago
lkito d8b89e2ef0 Added an optional feature to show bigger emoji-only messages with 3 or less emoji 5 years ago
Joseph Donofry 1c521d1711
Merge origin/master into reactions 5 years ago
Nicolas Werner bdf1147a80 add a bit of additional logging to image paste 5 years ago
Nicolas Werner 23e4408fa8 Add /roomnick command 5 years ago
Nicolas Werner 7de1fc62e8 Tag rooms via context menu 5 years ago
Nicolas Werner 5abdad308d Fix crash when reacting to messages from /sync 5 years ago
Nicolas Werner f0757a6426 Fix local echo for reactions 5 years ago
Nicolas Werner a75d7f00bc Fix reactions binding loop 5 years ago
Joseph Donofry f4ea0b215d
Attach emoji picker picked to reaction sender 5 years ago
Joseph Donofry a5778bdf40
Merge remote-tracking branch 'origin/master' into reactions 5 years ago
Joseph Donofry fca85bea39
Merge pull request #202 from aaronraimist/brew-libolm 5 years ago
Aaron Raimist 3338ecd39f
Remove MacPorts install instructions 5 years ago
Aaron Raimist 14eaad355e
Update macOS building dependencies now that libolm is available in Homebrew 5 years ago
DeepBlueV7.X 883567b0b9
Merge pull request #201 from lkito/master 5 years ago
lkito 2c21f6e3fa Implemented optional message body highlighting feature. 5 years ago
Nicolas Werner 3db9298e66 Bump mtxclient version 5 years ago
Nicolas Werner 73e0a9f453 Drop explicit ZLIB dependency 5 years ago
Nicolas Werner 576269c9e5 Prevent timeline being black on some platforms 5 years ago
Joseph Donofry ca20e2a98f
Remove accidental compiled translation commit 5 years ago
DeepBlueV7.X ce1c6a7df3
Merge pull request #198 from Chethan2k1/device-verification-dialogs 5 years ago
CH Chethan Reddy 57b8cdbd4d remove #if defined __has_include 5 years ago
CH Chethan Reddy e4a7e85935 Change the commit hash for mtxclient 5 years ago
CH Chethan Reddy 30d61e3114 minor fixes 5 years ago
DeepBlueV7.X 54e2e7ef7f Translated using Weblate (German) 5 years ago
Nicolas Werner 5a5b85cded Wait on maybe long running install script in CI 5 years ago
Nicolas Werner 10f0f7462a Try to fix arm builds hanging breaking CI 5 years ago
Joseph Donofry 6d2789f4d5
Merge master into reactions 5 years ago
Joseph Donofry eb4b02e8b9
Merge branch 'master' of ssh://github.com/Nheko-Reborn/nheko 5 years ago
Joseph Donofry 18f934efad
Add un-encrypted warning icon for messages in encrypted rooms 5 years ago
Nicolas Werner 6ff002b4ed Use standard cmake args instead of old -H 5 years ago
Joseph Donofry ff7468e6d5
Update emoji picker and translations 5 years ago
Nicolas Werner 279bcd1bf2 Show inline images 5 years ago
Nicolas Werner 2c3d09edbb Try to smooth scrolling a bit by increasing cacheBuffer 5 years ago
Nicolas Werner d6981355d3 Align scrolling to pixels manually 5 years ago
Joseph Donofry dfb76c693d
Update translations for new emoji picker 5 years ago
Joseph Donofry ee4dcef90f
Add new QML-based emoji picker (work in progress) 5 years ago
Nicolas Werner 004d10bfee Clip replies by default 5 years ago
Joseph Donofry 8984661187
Initial support for sending reactions 5 years ago
Nicolas Werner 000ab4853a Translation updates 5 years ago
Nicolas Werner 6befadeec8 Remove shadowing loginMethod 5 years ago
Nicolas Werner 9713284435 Fix not being able to log http status on login 5 years ago
Nicolas Werner 15716f5a34 bump mtxclient for SSO 5 years ago
Nicolas Werner 7b1fa60cc6 Add SSO 5 years ago
Nicolas Werner 813790e603 Improve Login and Register page hinting 5 years ago
DeepBlueV7.X 197f702dd0
Merge pull request #195 from jonnius/patch-1 5 years ago
jonnius 5f9ce33562 Update Ubuntu install and build instructions 5 years ago
Joseph Donofry 991fa4ac98
Change redaction color that accidentally got committed 5 years ago
Joseph Donofry 5b9611dcd3
Merge branch 'master' into reactions 5 years ago
Joseph Donofry cc92161339
Fix redaction color change that accidentally got committed 5 years ago
Joseph Donofry a1661f7006
merge master into reactions 5 years ago
DeepBlueV7.X 7beaf868ef
Merge pull request #192 from Nheko-Reborn/reactions 5 years ago
Nicolas Werner a942ae9b93 Fix messages being immediately read again 5 years ago
Nicolas Werner 0b1d3a40f4 Remove unused binding name 5 years ago
Nicolas Werner e48dfd15fe Add shortcuts to select/deselect reply targets 5 years ago
Nicolas Werner e5a5a66716 Fix reaction redaction for real this time 5 years ago
Nicolas Werner ff54ce9334 Fix rooms with a lot of reactions not paginating correctly 5 years ago
Nicolas Werner e045e3eb1c Show displayname on reactions 5 years ago
Nicolas Werner 8348a6c35d Fix reaction count 5 years ago
Nicolas Werner a1951056da Reaction and Button layout fixes 5 years ago
Nicolas Werner e55a09906f Misc color fixes 5 years ago
Nicolas Werner ca5490074a Fix scroll to replied to message (somewhat) 5 years ago
Nicolas Werner 692c6119b4 Fix joined rooms dropping to the bottom at first 5 years ago
Nicolas Werner db24f174b1 Fix jumpy room list, when sort order is ambiguous 5 years ago
Nicolas Werner 846ff33ed8 Position reaction emoji and text on the same baseline 5 years ago
Nicolas Werner 54013e4a00 Basic, broken reaction display 5 years ago
Nicolas Werner a9aed09d35 Add placeholder for reactions 5 years ago
Nicolas Werner a8b22e49c3 Fix self counting as read message 5 years ago
Nicolas Werner 0c73c74574 Speed up initial load a bit 5 years ago
DeepBlueV7.X ccd55c70df
Merge pull request #189 from Nheko-Reborn/db-migrations 5 years ago
Nicolas Werner d6685e8d61 Add delete pending_receipts migration 5 years ago
Nicolas Werner dad2de7ba2 Add support for db migrations 5 years ago
DeepBlueV7.X 38417a374d
Merge pull request #184 from shocklateboy92/features/backlog-progress 5 years ago
Lasath Fernando 166ed0674d Run linter 5 years ago
Nicolas Werner 9750241e73 Old Qt doesn't have .get... 5 years ago
DeepBlueV7.X 00c4d2629a
Merge pull request #188 from Nheko-Reborn/optimize-cpu-usage 5 years ago
Nicolas Werner 641a883bfd Optimize RoomList sorting 5 years ago
Nicolas Werner e6fcccc8bd Don't store pending receipts in cache 5 years ago
Nicolas Werner 2997155f56 Fix spacing of typing notifications 5 years ago
Nicolas Werner 0716bbafff Strip reply fallback from plain text body 5 years ago
Lasath Fernando 21b33363ce Make pagination spinner inline 5 years ago
Lasath Fernando 1f9d3024b1 Add visual indication that data is being fetched 5 years ago
DeepBlueV7.X 19ec8d261d
Merge pull request #183 from shocklateboy92/features/smooth-scroll 5 years ago
Nicolas Werner 3226d1787c Fix some smaller coloring issues 5 years ago
Nicolas Werner bb66254c58 Make notifications a bit easier to read 5 years ago
Nicolas Werner 27350cf51e Fix high CPU usage on high dpi screens 5 years ago
Lasath Fernando c5d373e8f1 Address CR comments 5 years ago
Lasath Fernando 5c57de070e Remove commented out code; fix padding for Pill 5 years ago
Lasath Fernando 84c5ff0bcb Fix binding loops for height 5 years ago
Lasath Fernando 1555dc2296 Shamelessly steal `ScrollHelper.qml` from spectral 5 years ago
Nicolas Werner 17c657a170 Send correct orientation for exif rotated images 5 years ago
Nicolas Werner dbb13dfad6 Update Changelog 5 years ago
Nicolas Werner d6386546b3 Only prune old messages every 500 syncs 5 years ago
Nicolas Werner 28adc9dc9b Respect exif rotation of images 5 years ago
Nicolas Werner d94ac86816 Fix double percent encoding of invites 5 years ago
Joseph Donofry a5d5ea1881
Fix URL in appdata.xml 5 years ago
Joseph Donofry 14ce840649 Update changelog 5 years ago
Joseph Donofry 619d9bc185 Merge remote-tracking branch 'origin/master' into release-prep-0.7.1 5 years ago
Joseph Donofry 4e3b190ac7
Merge pull request #173 from Nheko-Reborn/decrypt-sidebar 5 years ago
Joseph Donofry 5893365439 Change decrypt setting again 5 years ago
Joseph Donofry 18557023d9 Fix CHANGELOG typo 5 years ago
Joseph Donofry 87c06f0fc9 Merge branch 'decrypt-sidebar' of ssh://github.com/Nheko-Reborn/nheko into decrypt-sidebar 5 years ago
Joseph Donofry 994edb836c Change how decrypt value is retrieved from settings 5 years ago
Nicolas Werner ddcab64315 Update versions to 0.7.1 5 years ago
Joseph Donofry 69b0b68f92
Merge branch 'master' into decrypt-sidebar 5 years ago
Nicolas Werner b00e624fb0 Fix own messages not showing as encrypted 5 years ago
Nicolas Werner d5e1475a5c Fix encrypted messages not showing a user in the sidebar 5 years ago
Nicolas Werner 4720d2b562 Update translations 5 years ago
Joseph Donofry b298f01d92
Add signal/slot for decryption changes in last patch 5 years ago
Nicolas Werner 35601b6bef Terminate user color calculation, when no solution can be found 5 years ago
Joseph Donofry a4c280a4f9
Add ability to toggle sidebar messages for encrypted rooms 5 years ago
Nicolas Werner 0e1bb5137b Add menu entry to show decrypted source 5 years ago
Nicolas Werner 69c8476cb7 Update changelog 5 years ago
Nicolas Werner ba3d405513 Fix Binding restoreMode warning spamming logs 5 years ago
Nicolas Werner 6a39b29acc Disable opening images, when they are not loaded 5 years ago
Nicolas Werner e8271acd99 Fix own encrypted images not showing up after sending 5 years ago
DeepBlueV7.X 22ecdfc3ff
Merge pull request #169 from not-chicken/master 5 years ago
Lorem b4622f510e Add repology badge to readme 5 years ago
Joseph Donofry 03a838709b
Update appdata.xml 5 years ago
Joe 0768a70e75 Fix flatpak builds referencing wrong branch 5 years ago
Joe 9d46eb123f Release v0.7.0 5 years ago
Nicolas Werner 6e1c57a702 Prepare 0.7.0 release 5 years ago
Nicolas Werner eff8af6fac Try to fix messages getting stuck by sometimes sending them twice and never failing them 5 years ago
DeepBlueV7.X 076a1c3607
Merge pull request #167 from jevolk/master 5 years ago
Jason Volk 1eede32a8b Add missing qml-module-qtgraphicaleffects and qml-module-qtquick-controls2 to Ubuntu 19.10 in README. 5 years ago
Nicolas Werner 695c3c0814 Fix registration on construct 5 years ago
Nicolas Werner 70b0cbcd0f Pull in parsing fixes in mtxclient 5 years ago
Nicolas Werner a32bf3d4fc Add minimum cmark version to CMakeList 5 years ago
Nicolas Werner dd1dca3751 Add --debug switch 5 years ago
Nicolas Werner a70044e67b Cache decrypted events 5 years ago
Nicolas Werner c739a0422d Fix encrypted event indicator 5 years ago
Nicolas Werner 277c0e9564 Update translations 5 years ago
DeepBlueV7.X 46f3c97879 Translated using Weblate (English) 5 years ago
DeepBlueV7.X 741da4ab32 Translated using Weblate (German) 5 years ago
Nicolas Werner 872507002a Bump mtxclient to v0.3.0-rc1 5 years ago
Nicolas Werner 3ae4e49b5f Remove unused paramters 5 years ago
Nicolas Werner dcddea6fb8 Fix reply not closing 5 years ago
Nicolas Werner 82ec022f9c Fix notification not being cleared, when read event didn't cause a notification 5 years ago
Nicolas Werner 2b24a978e1 Mid color is used for scrollbars it seems 5 years ago
Nicolas Werner 2022775dd0 Clear notifications when event is read 5 years ago
Nicolas Werner 537cc966cc Use Item for transparent rectangles 5 years ago
Nicolas Werner d68b24188f Size reply background by contents 5 years ago
Nicolas Werner d2e495532d Make Filemessage background size by content 5 years ago
adasauce 5f7f564e57
Merge pull request #161 from Nheko-Reborn/palettes 5 years ago
Nicolas Werner 4fa12b52aa Add comment on why we surpress some of those log messages 5 years ago
Nicolas Werner 6794b6472d Add a few missing palette colors 5 years ago
Nicolas Werner 31a5972f2a Fix UserProfile resizing 5 years ago
Nicolas Werner 94f5b25888 Change fallback style to fusion 5 years ago
Nicolas Werner 8bf25f3d80 Set palette globally 5 years ago
Nicolas Werner d35cc3dda4 Use palette for qml code for theming 5 years ago
DeepBlueV7.X 384ef13c76
Merge pull request #160 from cloudrac3r/ubuntu-19.10-dependencies 5 years ago
Cadence Ember fd1825b7c0
Update Ubuntu 19.10 dependencies list 5 years ago
DeepBlueV7.X e389588bc2
Merge pull request #157 from Alch-Emi/no-escape 5 years ago
Emi Simpson a9bbea881e
Removed double-sanitization of ... is typing names 5 years ago
DeepBlueV7.X 0913887546
Merge pull request #156 from abma/patch-1 5 years ago
abma 5e14bbd1cd
fix #155: use correct subsystem on windows 5 years ago
Nicolas Werner 95272ce4ab Try to fix avatars not updating after deletion 5 years ago
Nicolas Werner 7a2b996f18 Fix high CPU usage on 5XX error codes 5 years ago
Nicolas Werner 7df6529310 Fix avatar not being set for some rooms in the top bar 5 years ago
Nicolas Werner b25e481418 Clean up unused avatar functions 5 years ago
Nicolas Werner 31a83c515e Fix broken avatars in quick switcher 5 years ago
DeepBlueV7.X c32a8bc226
Merge pull request #154 from adasauce/image-modal-download 5 years ago
Adasauce d0a1e81f43
Better image overlay handling when downloading 5 years ago
DeepBlueV7.X b41e2e6f18
Merge pull request #146 from Alch-Emi/priority-sort 5 years ago
Emi Simpson 5c308b1caf
Fixed bug caused by me forgetting what types im using (invites appearing at the bottom of the list instead of the top when priority sorting was off) 5 years ago
Emi Simpson 06676cfb33
Updated translations 5 years ago
Emi Simpson d51cbe7e32
Place invites on top of room list even when sorting by importance is off 5 years ago
Emi Simpson 78ac902549
Clean up stray comment 5 years ago
Emi Simpson 12aa94ad9a
Fixed compatibility with Qt < 5.11 5 years ago
Emi Simpson bf5ae884de
Make toggle in settings revert between old behavior and new behavior for sorting by unreads 5 years ago
Emi Simpson abac4c8d34
Sort room list on setting change 5 years ago
Emi Simpson 2320bfea26
Foiled by the linter once more 5 years ago
Emi Simpson 6bbe8ade0f
Made merge compatible with master 5 years ago
Emi Simpson b6bd36ac16
Added toggle in the settings to ignore minor events 5 years ago
Emi Simpson 81c9cb5c79
Switched room importance to an enum 5 years ago
Emi Simpson a5b388db15
Appease the linter 5 years ago
Emi Simpson 389117f1e8
Add a comment explaining room importance 5 years ago
Emi Simpson 0153dc7a39
Automatically move rooms down in the list once they've been read 5 years ago
Emi Simpson b2a6232eb3
Fixed channels appearing out of order when only mentions were present, without any non-mentions 5 years ago
Emi Simpson 08125e8c44
Sort room list by room priority 5 years ago
DeepBlueV7.X 5e991af57f
Merge pull request #145 from PC25/master 5 years ago
Pushpam Choudhary 93bcdbed2d Update minor fixes in translations as requested 5 years ago
Pushpam Choudhary 71ba8cb284 Add periods to terminate non-user messages in the timeline 5 years ago
Nicolas Werner 0f1a352dd7 Make buttons hideable via setting, button contents can still be accessed via context menu 5 years ago
Nicolas Werner 69a50c15c7 Reduce times we read upload data 5 years ago
Nicolas Werner a071f55c7b Fix images without size 5 years ago
DeepBlueV7.X baaa687d33
Merge pull request #142 from Alch-Emi/master 5 years ago
DeepBlueV7.X 50eec6a96b Translated using Weblate (German) 5 years ago
Emi Simpson 3fddf69958
Don't request access to the user's home directory 5 years ago
DeepBlueV7.X fc2f08a186
Merge pull request #137 from Nheko-Reborn/blurhash 5 years ago
Nicolas Werner 31a403a68e Update mtxclient in flatpak for blurhash 5 years ago
Nicolas Werner b894ce4dcd Make blurhash provider async 5 years ago
Nicolas Werner 5ac18f1f5f Speed up blurhash code 5 years ago
Nicolas Werner 0fc98b2692 Experimental blurhash implementation (MXC2448) 5 years ago
  1. 1
      .ci/install.sh
  2. 24
      .ci/script.sh
  3. 2
      .gitignore
  4. 39
      .travis.yml
  5. 117
      CHANGELOG.md
  6. 123
      CMakeLists.txt
  7. 58
      README.md
  8. 42
      appveyor.yml
  9. 2
      cmake/Hunter/config.cmake
  10. 11
      cmake/HunterGate.cmake
  11. 2
      cmake/Translations.cmake
  12. 18
      io.github.NhekoReborn.Nheko.json
  13. BIN
      resources/icons/ui/end-call.png
  14. BIN
      resources/icons/ui/microphone-mute.png
  15. BIN
      resources/icons/ui/microphone-unmute.png
  16. BIN
      resources/icons/ui/place-call.png
  17. BIN
      resources/icons/ui/search.png
  18. BIN
      resources/icons/ui/search@2x.png
  19. BIN
      resources/icons/ui/unlock.png
  20. BIN
      resources/icons/ui/unlock@2x.png
  21. 1
      resources/langs/.gitignore
  22. 2243
      resources/langs/nheko_cs.ts
  23. 1170
      resources/langs/nheko_de.ts
  24. 1542
      resources/langs/nheko_el.ts
  25. 1154
      resources/langs/nheko_en.ts
  26. 2252
      resources/langs/nheko_eo.ts
  27. 2257
      resources/langs/nheko_et.ts
  28. 1370
      resources/langs/nheko_fi.ts
  29. 1638
      resources/langs/nheko_fr.ts
  30. 2249
      resources/langs/nheko_it.ts
  31. 1108
      resources/langs/nheko_ja.ts
  32. 1404
      resources/langs/nheko_nl.ts
  33. 1412
      resources/langs/nheko_pl.ts
  34. 2241
      resources/langs/nheko_pt_PT.ts
  35. 2243
      resources/langs/nheko_ro.ts
  36. 1402
      resources/langs/nheko_ru.ts
  37. 2241
      resources/langs/nheko_si.ts
  38. 1412
      resources/langs/nheko_zh_CN.ts
  39. 5
      resources/media/README.txt
  40. BIN
      resources/media/callend.ogg
  41. BIN
      resources/media/ring.ogg
  42. BIN
      resources/media/ringback.ogg
  43. 33
      resources/nheko.appdata.xml
  44. 113
      resources/qml/ActiveCallBar.qml
  45. 60
      resources/qml/Avatar.qml
  46. 28
      resources/qml/EncryptionIndicator.qml
  47. 21
      resources/qml/ImageButton.qml
  48. 29
      resources/qml/MatrixText.qml
  49. 95
      resources/qml/Reactions.qml
  50. 108
      resources/qml/ScrollHelper.qml
  51. 48
      resources/qml/StatusIndicator.qml
  52. 130
      resources/qml/TimelineRow.qml
  53. 487
      resources/qml/TimelineView.qml
  54. 175
      resources/qml/UserProfile.qml
  55. 34
      resources/qml/delegates/FileMessage.qml
  56. 58
      resources/qml/delegates/ImageMessage.qml
  57. 252
      resources/qml/delegates/MessageDelegate.qml
  58. 4
      resources/qml/delegates/NoticeMessage.qml
  59. 7
      resources/qml/delegates/Pill.qml
  60. 143
      resources/qml/delegates/PlayableMediaMessage.qml
  61. 24
      resources/qml/delegates/Reply.qml
  62. 7
      resources/qml/delegates/TextMessage.qml
  63. 46
      resources/qml/device-verification/AwaitingVerificationConfirmation.qml
  64. 144
      resources/qml/device-verification/DeviceVerification.qml
  65. 69
      resources/qml/device-verification/DigitVerification.qml
  66. 33
      resources/qml/device-verification/EmojiElement.qml
  67. 414
      resources/qml/device-verification/EmojiVerification.qml
  68. 56
      resources/qml/device-verification/Failed.qml
  69. 46
      resources/qml/device-verification/NewVerificationRequest.qml
  70. 38
      resources/qml/device-verification/Success.qml
  71. 56
      resources/qml/device-verification/Waiting.qml
  72. 66
      resources/qml/device-verification/sas-emoji.json
  73. 16
      resources/qml/emoji/EmojiButton.qml
  74. 332
      resources/qml/emoji/EmojiPicker.qml
  75. 2
      resources/qtquickcontrols2.conf
  76. 29
      resources/res.qrc
  77. 21
      resources/styles/nheko-dark.qss
  78. 15
      resources/styles/nheko.qss
  79. 18
      resources/styles/system.qss
  80. 17
      scripts/emoji_codegen.py
  81. 38
      scripts/generate_icns.sh
  82. 19
      src/AvatarProvider.cpp
  83. 38
      src/BlurhashProvider.cpp
  84. 51
      src/BlurhashProvider.h
  85. 1794
      src/Cache.cpp
  86. 46
      src/Cache.h
  87. 50
      src/CacheCryptoStructs.h
  88. 12
      src/CacheStructs.h
  89. 165
      src/Cache_p.h
  90. 457
      src/CallManager.cpp
  91. 76
      src/CallManager.h
  92. 627
      src/ChatPage.cpp
  93. 52
      src/ChatPage.h
  94. 12
      src/CommunitiesList.cpp
  95. 1
      src/CommunitiesList.h
  96. 2
      src/CommunitiesListItem.cpp
  97. 1
      src/CommunitiesListItem.h
  98. 20
      src/CompletionModel.h
  99. 2
      src/Config.h
  100. 794
      src/DeviceVerificationFlow.cpp
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,6 +3,7 @@
set -ex
if [ "$FLATPAK" ]; then
sudo apt-get -y install flatpak flatpak-builder elfutils
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --noninteractive install --user flathub org.kde.Platform//5.14
flatpak --noninteractive install --user flathub org.kde.Sdk//5.14

@ -6,8 +6,27 @@ if [ "$FLATPAK" ]; then
mkdir -p build-flatpak
cd build-flatpak
flatpak-builder --ccache --repo=repo --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json
flatpak build-bundle repo nheko-${VERSION}-${ARCH}.flatpak io.github.NhekoReborn.Nheko 0.7.0-dev
jobsarg=""
if [ "$ARCH" = "arm64" ]; then
jobsarg="--jobs=2"
fi
flatpak-builder --ccache --repo=repo --subject="Build of Nheko ${VERSION} $jobsarg `date`" app ../io.github.NhekoReborn.Nheko.json &
# to prevent flatpak builder from timing out on arm, run it in the background and print something every minute for up to 30 minutes.
minutes=0
limit=40
while kill -0 $! >/dev/null 2>&1; do
if [ $minutes == $limit ]; then
break;
fi
minutes=$((minutes+1))
sleep 60
done
flatpak build-bundle repo nheko-${VERSION}-${ARCH}.flatpak io.github.NhekoReborn.Nheko master
mkdir ../artifacts
mv nheko-*.flatpak ../artifacts
@ -52,7 +71,6 @@ cmake -GNinja -H. -Bbuild \
-DHUNTER_ROOT=".hunter" \
-DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo \
-DUSE_BUNDLED_OPENSSL=OFF \
-DCI_BUILD=ON
fi
cmake --build build

2
.gitignore vendored

@ -6,6 +6,7 @@ cscope*
/.ccls-cache
/.exrc
.gdb_history
.hunter
# GTAGS
GTAGS
@ -72,6 +73,7 @@ install_manifest.txt
# Icon must end with two \r
Icon
# Thumbnails
._*

@ -63,21 +63,21 @@ matrix:
env:
- CXX=g++-8
- CC=gcc-8
- QT_PKG=59
- QT_PKG=510
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- sourceline: 'ppa:beineri/opt-qt597-xenial'
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial'
packages:
- g++-8
- ninja-build
- qt59base
- qt59tools
- qt59svg
- qt59multimedia
- qt59quickcontrols2
- qt59graphicaleffects
- qt510base
- qt510tools
- qt510svg
- qt510multimedia
- qt510quickcontrols2
- qt510graphicaleffects
- liblmdb-dev
- libgl1-mesa-dev # needed for missing gl.h
- os: linux
@ -85,23 +85,23 @@ matrix:
env:
- CXX=clang++-6.0
- CC=clang-6.0
- QT_PKG=59
- QT_PKG=510
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- llvm-toolchain-xenial-6.0
- sourceline: 'ppa:beineri/opt-qt597-xenial'
- sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial'
packages:
- clang++-6.0
- g++-7
- ninja-build
- qt59base
- qt59tools
- qt59svg
- qt59multimedia
- qt59quickcontrols2
- qt59graphicaleffects
- qt510base
- qt510tools
- qt510svg
- qt510multimedia
- qt510quickcontrols2
- qt510graphicaleffects
- liblmdb-dev
- libgl1-mesa-dev # needed for missing gl.h
- os: linux
@ -113,10 +113,6 @@ matrix:
apt:
sources:
- sourceline: 'ppa:alexlarsson/flatpak'
packages:
- flatpak
- flatpak-builder
- elfutils
- os: linux
arch: arm64
env:
@ -128,9 +124,6 @@ matrix:
sources:
- sourceline: 'ppa:alexlarsson/flatpak'
packages:
- flatpak
- flatpak-builder
- elfutils
- librsvg2-bin
before_install:

@ -1,25 +1,126 @@
# Changelog
## [Unreleased]
## [0.7.2] -- 2020-06-12
### Highlights
- Reactions
- React to a message with an emoji! 🎉
- Reactions are shown below a message in a small bubble with a counter.
- By clicking on that, others can add to the reaction count.
- It may help you celebrating a new Nheko Release or react with a 👎 to a failed build to express your frustration.
- This uses a new emoji picker. The picker will be improved in the near future (better scrolling, sections, favorites, recently used or similar) and then probably replace the current picker.
- Support for tagging rooms `[tag]`
- Assign custom tags to rooms from the context menu in the room list.
- This allows filtering rooms via the group list. This puts you in a focus mode showing only the selected tags.
- You can assign multiple tags to group rooms however you like.
- SSO Login
- With this you can now login on servers, that only provide SSO.
- Just enter any mxid on the server. Nheko will figure out that you need to use SSO and redirect your browser to the login page.
- Complete the login in your browser and Nheko should automatically log you in.
- Presence
- Shows online status of the people you are talking to.
- You can define a custom status message to tell others what you are currently up to.
- The status message appears next to the usernames in the timeline.
- Your server needs to have presence enabled for this to work.
### [0.7.0] -- Unreleased
### Features
- Respect exif rotation of images
- An italian translation (contributed by Lorenzo Ancora)
- Optional alerts in your taskbar (contributed by z33ky)
- Optional bigger emoji only messages in the timeline (contributed by lkito)
- Optional hover feedback on messages (contributed by lkito)
- `/roomnick` to change your displayname in a single room.
- Preliminary support for showing inline images.
- Warn about unencrypted messages in encrypted rooms.
### Improvements
- perf: Use less CPU to sort the room list.
- Limit size of replies. This currently looks a bit rough, but should improve in the future with a gradient or at some other transition.
- perf: Only clean out old messages from the database every 500 syncs. (There is usually more than one sync every second)
- Improve the login and register masks a bit with hints and validation.
- Descriptions for settings (contributed by lkito)
- A visual indicator, that nheko is fetching messages and improved scrolling (contributed by Lasath Fernando)
### Bugfixes
- Fix not being able to join rooms
- Fix scale factor setting
- Buildfixes against gcc10 and Qt5.15 (missing includes)
- Settings now apply immediately again after changing them (only exception should be the scale factor)
- Join messages should never have empty texts now
- Timeline should now fail to render less often on platforms with native sibling windows.
- Don't rescale images on every frame on highdpi screens.
### Upgrade Notes
<span style="color: red;">This updates includes some changes to the database. Older versions don't handle that gracefully and will delete your database. It is therefore recommended to not downgrade below this version!</span>
## [0.7.1] -- 2020-04-24
### Features
- Show decrypted message source (helps debugging)
- Allow user to show / hide messages in encrypted rooms in sidebar
### Bugfixes
- Fix display of images sent by the user (thank you, wnereiz and not-chicken for reporting)
- Fix crash when trying to maximize image, that wasn't downloaded yet.
- Fix Binding restoreMode flooding logs on Qt 5.14.2+
- Fix with some qml styles hidden menu items leave empty space
- Fix encrypted messages not showing a user in the sidebar
- Fix hangs when generating colors with some system theme color schemes (#172)
## [0.7.0] -- 2020-04-19
0.7.0 *requires* mtxclient 0.3.0. Make sure you compile against 0.3.0
if you do not use the mtxclient bundled with nheko.
#### Features
- Make nheko session import / export format match riot. Fixes #48 (WIP)
- Implement proper replies (WIP)
### Features
- Make nheko session import / export format match riot. Fixes #48
- Implement proper replies
- Add .well-known support for auto-completing homeserver information
- Add mentions viewer so you can see all the messages you have been mentioned in (WIP)
- Add mentions viewer so you can see all the messages you have been mentioned in
- Currently broken due to QML changes. Will be fixed in the future.
- Add emoji font selection preference
- Encryption and decryption of media in E2EE rooms
- Square avatars
- Support for muting and unmuting rooms
- Basic support for playing audio and video messages in the timeline
- Support for a lot more event types (hiding them will come in the future)
- Support for sending all messages as plain text
- Support for inviting, kicking, banning and unbanning users
- Sort the room list by importance of messages (thanks @Alch-Emi)
- Experimental support for [blurhashes](https://github.com/matrix-org/matrix-doc/pull/2448)
#### Improvements
### Improvements
- Add dedicated reply button to Timeline items. Add button for other options so
that right click isn't always required.
- Fix various things with regards to emoji rendering and the emoji picker (WIP)
- Fix various things with regards to emoji rendering and the emoji picker
- Lots and lots and lots of localization updates.
- Additional tweaks to the system theme
- Render timeline in Qml to drop memory usage
- Reduce memory usage of avatars
- Close notifications after they have been read on Linux
- Escape html properly in most places
- A lot of improvements around the image overlay
- The settings page now resizes properly for small screens
- Miscellaneous styling improvements
- Simplify and speedup build
- Display more emojis in the selected emoji font
- Use 'system' theme as default if QT_QPA_PLATFORMTHEME is set
### Bugfixes
- Fix messages stuck on unread
- Reduce the amount of messages shown as "xxx sent an encrypted message"
- Fix various race conditions and crashes
- Fix some compatibility issues with the construct homeserver
Be aware, that Nheko now requires Qt 5.10 and boost 1.70 or higher.
## [0.6.4] - 2019-05-22

@ -4,21 +4,23 @@ option(APPVEYOR_BUILD "Build on appveyor" OFF)
option(CI_BUILD "Set when building in CI. Enables -Werror where possible" OFF)
option(ASAN "Compile with address sanitizers" OFF)
option(QML_DEBUGGING "Enable qml debugging" OFF)
option(COMPILE_QML "Compile Qml. It will make Nheko faster, but you will need to recompile it, when you update Qt." OFF)
set(
CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/toolchain.cmake"
CACHE
FILEPATH "Default toolchain"
)
set(CMAKE_CXX_STANDARD 17 CACHE STRING "C++ standard")
set(CMAKE_CXX_STANDARD_REQUIRED ON CACHE BOOL "Require C++ standard to be supported")
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "compile as PIC by default")
option(HUNTER_ENABLED "Enable Hunter package manager" OFF)
include("cmake/HunterGate.cmake")
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.23.244.tar.gz"
SHA1 "2c0f491fd0b80f7b09e3d21adb97237161ef9835"
LOCAL
)
URL "https://github.com/cpp-pm/hunter/archive/v0.23.260.tar.gz"
SHA1 "13775235910a3fa85644568d1c5be8271de72e1c"
)
option(USE_BUNDLED_BOOST "Use the bundled version of Boost." ${HUNTER_ENABLED})
option(USE_BUNDLED_SPDLOG "Use the bundled version of spdlog."
@ -33,10 +35,6 @@ option(USE_BUNDLED_JSON "Use the bundled version of nlohmann json."
option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL."
${HUNTER_ENABLED})
option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED})
option(USE_BUNDLED_SODIUM "Use the bundled version of libsodium."
${HUNTER_ENABLED})
option(USE_BUNDLED_ZLIB "Use the bundled version of zlib."
${HUNTER_ENABLED})
option(USE_BUNDLED_LMDB "Use the bundled version of lmdb."
${HUNTER_ENABLED})
option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++."
@ -80,7 +78,7 @@ include(QtCommon)
project(nheko LANGUAGES CXX C)
set(CPACK_PACKAGE_VERSION_MAJOR "0")
set(CPACK_PACKAGE_VERSION_MINOR "7")
set(CPACK_PACKAGE_VERSION_PATCH "0")
set(CPACK_PACKAGE_VERSION_PATCH "2")
set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR})
set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR})
set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH})
@ -144,9 +142,9 @@ if (APPLE)
endif(APPLE)
if (Qt5Widgets_FOUND)
if (Qt5Widgets_VERSION VERSION_LESS 5.9.0)
if (Qt5Widgets_VERSION VERSION_LESS 5.10.0)
message(STATUS "Qt version ${Qt5Widgets_VERSION}")
message(WARNING "Minimum supported Qt5 version is 5.9!")
message(WARNING "Minimum supported Qt5 version is 5.10!")
endif()
endif(Qt5Widgets_FOUND)
@ -227,6 +225,7 @@ configure_file(cmake/nheko.h config/nheko.h)
#
set(SRC_FILES
# Dialogs
src/dialogs/AcceptCall.cpp
src/dialogs/CreateRoom.cpp
src/dialogs/FallbackAuth.cpp
src/dialogs/ImageOverlay.cpp
@ -235,20 +234,25 @@ set(SRC_FILES
src/dialogs/LeaveRoom.cpp
src/dialogs/Logout.cpp
src/dialogs/MemberList.cpp
src/dialogs/PlaceCall.cpp
src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp
src/dialogs/ReadReceipts.cpp
src/dialogs/RoomSettings.cpp
src/dialogs/UserProfile.cpp
# Emoji
src/emoji/Category.cpp
src/emoji/EmojiModel.cpp
src/emoji/ItemDelegate.cpp
src/emoji/Panel.cpp
src/emoji/PickButton.cpp
src/emoji/Provider.cpp
src/emoji/Provider_new.cpp
# Timeline
src/timeline/EventStore.cpp
src/timeline/Reaction.cpp
src/timeline/TimelineViewManager.cpp
src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp
@ -273,39 +277,45 @@ set(SRC_FILES
src/ui/ToggleButton.cpp
src/ui/Theme.cpp
src/ui/ThemeManager.cpp
src/ui/UserProfile.cpp
src/AvatarProvider.cpp
src/BlurhashProvider.cpp
src/Cache.cpp
src/CallManager.cpp
src/ChatPage.cpp
src/CommunitiesListItem.cpp
src/ColorImageProvider.cpp
src/CommunitiesList.cpp
src/CommunitiesListItem.cpp
src/DeviceVerificationFlow.cpp
src/EventAccessors.cpp
src/InviteeItem.cpp
src/LoginPage.cpp
src/Logging.cpp
src/LoginPage.cpp
src/MainWindow.cpp
src/MatrixClient.cpp
src/MxcImageProvider.cpp
src/ColorImageProvider.cpp
src/QuickSwitcher.cpp
src/Olm.cpp
src/QuickSwitcher.cpp
src/RegisterPage.cpp
src/RoomInfoListItem.cpp
src/RoomList.cpp
src/SSOHandler.cpp
src/SideBarActions.cpp
src/Splitter.cpp
src/popups/SuggestionsPopup.cpp
src/popups/PopupItem.cpp
src/popups/ReplyPopup.cpp
src/popups/UserMentions.cpp
src/TextInputWidget.cpp
src/TopRoomBar.cpp
src/TrayIcon.cpp
src/Utils.cpp
src/UserInfoWidget.cpp
src/UserSettingsPage.cpp
src/Utils.cpp
src/WebRTCSession.cpp
src/WelcomePage.cpp
src/popups/PopupItem.cpp
src/popups/SuggestionsPopup.cpp
src/popups/UserMentions.cpp
src/main.cpp
third_party/blurhash/blurhash.cpp
)
@ -318,14 +328,11 @@ find_package(Boost 1.70 REQUIRED
COMPONENTS iostreams
system
thread)
if(USE_BUNDLED_ZLIB)
hunter_add_package(ZLIB)
endif()
find_package(ZLIB REQUIRED)
if(USE_BUNDLED_OPENSSL)
hunter_add_package(OpenSSL)
endif()
find_package(OpenSSL REQUIRED)
find_package(OpenSSL 1.1.0 REQUIRED)
if(USE_BUNDLED_MTXCLIENT)
include(FetchContent)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
@ -333,11 +340,11 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 7fc1d357afaabb134cb6d9c593f94915973d31fa
GIT_TAG ad5575bc24089dc385e97d9ace026414b618775c
)
FetchContent_MakeAvailable(MatrixClient)
else()
find_package(MatrixClient 0.3.0 REQUIRED)
find_package(MatrixClient 0.3.1 REQUIRED)
endif()
if(USE_BUNDLED_OLM)
include(FetchContent)
@ -376,7 +383,7 @@ if(USE_BUNDLED_CMARK)
add_library(cmark::cmark ALIAS libcmark_static)
endif()
else()
find_package(cmark REQUIRED)
find_package(cmark REQUIRED 0.29.0)
endif()
if(USE_BUNDLED_JSON)
@ -419,14 +426,18 @@ else()
find_package(Tweeny REQUIRED)
endif()
include(FindPkgConfig)
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.14 gstreamer-webrtc-1.0>=1.14)
# single instance functionality
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
add_subdirectory(third_party/SingleApplication-3.0.19/)
add_subdirectory(third_party/SingleApplication-3.1.3.1/)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
qt5_wrap_cpp(MOC_HEADERS
# Dialogs
src/dialogs/AcceptCall.h
src/dialogs/CreateRoom.h
src/dialogs/FallbackAuth.h
src/dialogs/ImageOverlay.h
@ -435,20 +446,24 @@ qt5_wrap_cpp(MOC_HEADERS
src/dialogs/LeaveRoom.h
src/dialogs/Logout.h
src/dialogs/MemberList.h
src/dialogs/PlaceCall.h
src/dialogs/PreviewUploadOverlay.h
src/dialogs/RawMessage.h
src/dialogs/ReCaptcha.h
src/dialogs/ReadReceipts.h
src/dialogs/RoomSettings.h
src/dialogs/UserProfile.h
# Emoji
src/emoji/Category.h
src/emoji/EmojiModel.h
src/emoji/ItemDelegate.h
src/emoji/Panel.h
src/emoji/PickButton.h
src/emoji/Provider.h
# Timeline
src/timeline/EventStore.h
src/timeline/Reaction.h
src/timeline/TimelineViewManager.h
src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h
@ -472,34 +487,38 @@ qt5_wrap_cpp(MOC_HEADERS
src/ui/ToggleButton.h
src/ui/Theme.h
src/ui/ThemeManager.h
src/ui/UserProfile.h
src/notifications/Manager.h
src/AvatarProvider.h
src/BlurhashProvider.h
src/Cache_p.h
src/CallManager.h
src/ChatPage.h
src/CommunitiesListItem.h
src/CommunitiesList.h
src/CommunitiesListItem.h
src/DeviceVerificationFlow.h
src/InviteeItem.h
src/LoginPage.h
src/MainWindow.h
src/MxcImageProvider.h
src/InviteeItem.h
src/QuickSwitcher.h
src/RegisterPage.h
src/RoomInfoListItem.h
src/RoomList.h
src/SSOHandler.h
src/SideBarActions.h
src/Splitter.h
src/popups/SuggestionsPopup.h
src/popups/ReplyPopup.h
src/popups/PopupItem.h
src/popups/UserMentions.h
src/TextInputWidget.h
src/TopRoomBar.h
src/TrayIcon.h
src/UserInfoWidget.h
src/UserSettingsPage.h
src/WebRTCSession.h
src/WelcomePage.h
src/popups/PopupItem.h
src/popups/SuggestionsPopup.h
src/popups/UserMentions.h
)
#
@ -511,6 +530,9 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC})
if (APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa")
set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/emoji/MacHelper.mm)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
endif()
elseif (WIN32)
file(DOWNLOAD
"https://raw.githubusercontent.com/mohabouje/WinToast/41ed1c58d5dce0ee9c01dbdeac05be45358d4f57/src/wintoastlib.cpp"
@ -538,7 +560,12 @@ if(ASAN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined")
endif()
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
if(WIN32)
add_executable (nheko WIN32 ${OS_BUNDLE} ${NHEKO_DEPS})
else()
add_executable (nheko ${OS_BUNDLE} ${NHEKO_DEPS})
endif()
if(APPLE)
target_link_libraries (nheko PRIVATE Qt5::MacExtras)
elseif(WIN32)
@ -547,7 +574,7 @@ elseif(WIN32)
else()
target_link_libraries (nheko PRIVATE Qt5::DBus)
endif()
target_include_directories(nheko PRIVATE src includes)
target_include_directories(nheko PRIVATE src includes third_party/blurhash third_party/cpp-httplib-0.5.12)
target_link_libraries(nheko PRIVATE
MatrixClient::MatrixClient
@ -569,6 +596,18 @@ target_link_libraries(nheko PRIVATE
tweeny
SingleApplication::SingleApplication)
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
target_precompile_headers(nheko
PRIVATE
<string>
)
endif()
if (TARGET PkgConfig::GSTREAMER)
target_link_libraries(nheko PRIVATE PkgConfig::GSTREAMER)
target_compile_definitions(nheko PRIVATE GSTREAMER_AVAILABLE)
endif()
if(MSVC)
target_link_libraries(nheko PRIVATE ntdll)
endif()

@ -2,7 +2,7 @@ nheko
----
[![Build Status](https://travis-ci.org/Nheko-Reborn/nheko.svg?branch=master)](https://travis-ci.org/Nheko-Reborn/nheko)
[![Build status](https://ci.appveyor.com/api/projects/status/07qrqbfylsg4hw2h/branch/master?svg=true)](https://ci.appveyor.com/project/redsky17/nheko/branch/master)
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.6.4)
[![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/v0.7.2)
[![Nightly](https://img.shields.io/badge/download-nightly-green.svg)](https://matrix-static.neko.dev/room/!TshDrgpBNBDmfDeEGN:neko.dev/)
[![#nheko-reborn:matrix.org](https://img.shields.io/matrix/nheko-reborn:matrix.org.svg?label=%23nheko-reborn:matrix.org)](https://matrix.to/#/#nheko-reborn:matrix.org)
[![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko)
@ -51,12 +51,14 @@ can be found in the [Github releases](https://github.com/Nheko-Reborn/nheko/rele
### Repositories
[![Packaging status](https://repology.org/badge/tiny-repos/nheko.svg)](https://repology.org/project/nheko/versions)
#### Arch Linux
```bash
pacaur -S nheko # nheko-git
```
#### Debian (10 and above)
#### Debian (10 and above) / Ubuntu (18.04 and above)
```bash
sudo apt install nheko
@ -73,6 +75,14 @@ sudo eselect repository enable matrix
sudo emerge -a nheko
```
#### Nix(os)
```bash
nix-env -iA nixpkgs.nheko
# or
nix-shell -p nheko --run nheko
```
#### Alpine Linux (and postmarketOS)
Make sure you have the testing repositories from `edge` enabled. Note that this is not needed on postmarketOS.
@ -95,11 +105,10 @@ guix install nheko
#### macOS (10.14 and above)
with [macports](https://www.macports.org/) :
with [homebrew](https://brew.sh/):
```sh
sudo port install nheko
brew cask install nheko
```
### Build Requirements
@ -112,8 +121,7 @@ sudo port install nheko
- [LMDB](https://symas.com/lightning-memory-mapped-database/)
- [cmark](https://github.com/commonmark/cmark) 0.29 or greater.
- Boost 1.70 or greater.
- [libolm](https://git.matrix.org/git/olm)
- [libsodium](https://github.com/jedisct1/libsodium)
- [libolm](https://gitlab.matrix.org/matrix-org/olm)
- [spdlog](https://github.com/gabime/spdlog)
- A compiler that supports C++ 17:
- Clang 6 (tested on Travis CI)
@ -124,6 +132,7 @@ Nheko can use bundled version for most of those libraries automatically, if the
To use them, you can enable the hunter integration by passing `-DHUNTER_ENABLED=ON`.
It is probably wise to link those dependencies statically by passing `-DBUILD_SHARED_LIBS=OFF`
You can select which bundled dependencies you want to use py passing various `-DUSE_BUNDLED_*` flags. By default all dependencies are bundled *if* you enable hunter.
If you experience build issues and you are trying to link `mtxclient` library without hunter, make sure the library version(commit) as mentioned in the `CMakeList.txt` is used. Sometimes we have to make breaking changes in `mtxclient` and for that period the master branch of both repos may not be compatible.
The bundle flags are currently:
@ -135,15 +144,13 @@ The bundle flags are currently:
- USE_BUNDLED_JSON
- USE_BUNDLED_OPENSSL
- USE_BUNDLED_MTXCLIENT
- USE_BUNDLED_SODIUM
- USE_BUNDLED_ZLIB
- USE_BUNDLED_LMDB
- USE_BUNDLED_LMDBXX
- USE_BUNDLED_TWEENY
#### Linux
If you don't want to install any external dependencies, you can generate an AppImage locally using docker.
If you don't want to install any external dependencies, you can generate an AppImage locally using docker. It is not that well maintained though...
```bash
make docker-app-image
@ -161,39 +168,30 @@ sudo pacman -S qt5-base \
fontconfig \
lmdb \
cmark \
boost \
libsodium
boost
```
##### Gentoo Linux
```bash
sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig
```
##### Ubuntu 16.04
```bash
sudo add-apt-repository ppa:beineri/opt-qt592-xenial
sudo add-apt-repository ppa:george-edison55/cmake-3.x
sudo add-apt-repository ppa:ubuntu-toolchain-r-test
sudo apt-get update
sudo apt-get install -y g++-7 qt59base qt59svg qt59tools qt59multimedia cmake liblmdb-dev libsodium-dev
sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig
```
##### Ubuntu 19.10
##### Ubuntu 20.04
```bash
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
sudo apt install g++-7 cmake liblmdb-dev libsodium-dev qt{base,tools,multimedia}5-dev qml-module-qt{gstreamer,multimedia,quick-extras} libqt5svg5-dev qt{script,quickcontrols2-}5-dev
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,graphicaleffects,quick-controls2}
```
This will install all dependencies, except for tweeny (use bundled tweeny)
and mtxclient (needs to be build separately).
##### Debian Buster (or higher probably)
(User report, not sure if all of those are needed)
```bash
sudo apt install cmake gcc make automake liblmdb-dev libsodium-dev \
sudo apt install cmake gcc make automake liblmdb-dev \
qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \
qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools \
qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts
@ -210,7 +208,7 @@ guix environment nheko
```bash
brew update
brew install qt5 lmdb cmake llvm libsodium spdlog boost cmark
brew install qt5 lmdb cmake llvm spdlog boost cmark libolm
```
##### Windows
@ -229,14 +227,14 @@ Make sure to install the `MSVC 2017 64-bit` toolset for at least Qt 5.10
We can now build nheko:
```bash
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build
```
To use bundled dependencies you can use hunter, i.e.:
```bash
cmake -H. -Bbuild -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=OFF
cmake -S. -Bbuild -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=OFF
cmake --build build --config Release
```
@ -255,7 +253,7 @@ You might need to pass `-DCMAKE_PREFIX_PATH` to cmake to point it at your qt5 in
e.g on macOS
```
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5)
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5)
cmake --build build
```

@ -1,6 +1,6 @@
---
version: 0.7.0-{build}
version: 0.7.2-{build}
configuration: Release
image: Visual Studio 2017
@ -30,8 +30,8 @@ build_script:
# VERSION format: branch-master/branch-1.2
# INSTVERSION format: x.y.z
# WINVERSION format: 9999.0.0.123/1.2.0.234
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.6.4
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.6.4
- if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.7.2
- if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.7.2
- if "%APPVEYOR_REPO_TAG%"=="false" if "%APPVEYOR_REPO_BRANCH%"=="master" set INSTVERSION=9999.0
- if "%APPVEYOR_REPO_TAG%"=="false" set WINVERSION=%INSTVERSION%.0.%APPVEYOR_BUILD_NUMBER%
# VERSION format: v1.2.3/v1.3.4
@ -74,31 +74,31 @@ after_build:
- mkdir installer
- mkdir installer\config
- mkdir installer\packages
- mkdir installer\packages\com.mujx.nheko
- mkdir installer\packages\com.mujx.nheko\data
- mkdir installer\packages\com.mujx.nheko\meta
- mkdir installer\packages\com.mujx.nheko.cleanup\meta
- mkdir installer\packages\io.github.nhekoreborn.nheko
- mkdir installer\packages\io.github.nhekoreborn.nheko\data
- mkdir installer\packages\io.github.nhekoreborn.nheko\meta
- mkdir installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
# Copy installer data
- copy %BUILD%\resources\nheko.ico installer\config
- copy %BUILD%\resources\nheko.png installer\config
- copy %BUILD%\COPYING installer\packages\com.mujx.nheko\meta\license.txt
- copy %BUILD%\COPYING installer\packages\com.mujx.nheko.cleanup\meta\license.txt
- copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko\meta\license.txt
- copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\license.txt
- copy %BUILD%\deploy\installer\config.xml installer\config
- copy %BUILD%\deploy\installer\controlscript.qs installer\config
- copy %BUILD%\deploy\installer\uninstall.qs installer\packages\com.mujx.nheko\data
- copy %BUILD%\deploy\installer\gui\package.xml installer\packages\com.mujx.nheko\meta
- copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\com.mujx.nheko\meta
- copy %BUILD%\deploy\installer\cleanup\package.xml installer\packages\com.mujx.nheko.cleanup\meta
- copy %BUILD%\deploy\installer\cleanup\installscript.qs installer\packages\com.mujx.nheko.cleanup\meta
- copy %BUILD%\deploy\installer\uninstall.qs installer\packages\io.github.nhekoreborn.nheko\data
- copy %BUILD%\deploy\installer\gui\package.xml installer\packages\io.github.nhekoreborn.nheko\meta
- copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\io.github.nhekoreborn.nheko\meta
- copy %BUILD%\deploy\installer\cleanup\package.xml installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
- copy %BUILD%\deploy\installer\cleanup\installscript.qs installer\packages\io.github.nhekoreborn.nheko.cleanup\meta
# Amend version and date
- sed -i "s/__VERSION__/0.6.4/" installer\config\config.xml
- sed -i "s/__VERSION__/0.6.4/" installer\packages\com.mujx.nheko\meta\package.xml
- sed -i "s/__VERSION__/0.6.4/" installer\packages\com.mujx.nheko.cleanup\meta\package.xml
- sed -i "s/__DATE__/%DATE%/" installer\packages\com.mujx.nheko\meta\package.xml
- sed -i "s/__DATE__/%DATE%/" installer\packages\com.mujx.nheko.cleanup\meta\package.xml
- sed -i "s/__VERSION__/0.7.2/" installer\config\config.xml
- sed -i "s/__VERSION__/0.7.2/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
- sed -i "s/__VERSION__/0.7.2/" installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\package.xml
- sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
- sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko.cleanup\meta\package.xml
# Copy nheko data
- xcopy NhekoData\*.* installer\packages\com.mujx.nheko\data\*.* /s /e /c /y
- move NhekoRelease\nheko.exe installer\packages\com.mujx.nheko\data
- xcopy NhekoData\*.* installer\packages\io.github.nhekoreborn.nheko\data\*.* /s /e /c /y
- move NhekoRelease\nheko.exe installer\packages\io.github.nhekoreborn.nheko\data
- mkdir tools
- curl -L -O https://download.qt.io/official_releases/qt-installer-framework/3.0.4/QtInstallerFramework-win-x86.exe
- 7z x QtInstallerFramework-win-x86.exe -otools -aoa

@ -1,5 +1,5 @@
hunter_config(
Boost
VERSION "1.70.0-p0"
VERSION "1.70.0-p1"
CMAKE_ARGS IOSTREAMS_NO_BZIP2=1
)

@ -133,10 +133,14 @@ function(hunter_gate_self root version sha1 result)
string(SUBSTRING "${sha1}" 0 7 archive_id)
if(EXISTS "${root}/cmake/Hunter")
set(hunter_self "${root}")
else()
set(
hunter_self
"${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked"
)
endif()
set("${result}" "${hunter_self}" PARENT_SCOPE)
endfunction()
@ -490,6 +494,12 @@ macro(HunterGate)
)
set(_master_location "${_hunter_self}/cmake/Hunter")
if(EXISTS "${HUNTER_GATE_ROOT}/cmake/Hunter")
# Hunter downloaded manually (e.g. by 'git clone')
set(_unused "xxxxxxxxxx")
set(HUNTER_GATE_SHA1 "${_unused}")
set(HUNTER_GATE_VERSION "${_unused}")
else()
get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE)
set(_done_location "${_archive_id_location}/DONE")
set(_sha1_location "${_archive_id_location}/SHA1")
@ -522,6 +532,7 @@ macro(HunterGate)
"try to update Hunter/HunterGate"
)
endif()
endif()
include("${_master_location}")
set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES)
endif()

@ -21,7 +21,7 @@ if(NOT EXISTS ${_qrc})
endif()
qt5_add_resources(LANG_QRC ${_qrc})
if(Qt5QuickCompiler_FOUND)
if(Qt5QuickCompiler_FOUND AND COMPILE_QML)
qtquick_compiler_add_resources(QRC resources/res.qrc)
else()
qt5_add_resources(QRC resources/res.qrc)

@ -1,7 +1,7 @@
{
"id": "io.github.NhekoReborn.Nheko",
"command": "nheko",
"branch": "0.7.0-dev",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "5.14",
"sdk": "org.kde.Sdk",
@ -10,7 +10,6 @@
"rename-appdata-file": "nheko.appdata.xml",
"finish-args": [
"--device=dri",
"--filesystem=home",
"--share=ipc",
"--share=network",
"--socket=pulseaudio",
@ -74,9 +73,9 @@
],
"sources": [
{
"sha256": "3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19",
"sha256": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb",
"type": "archive",
"url": "https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz"
"url": "https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz"
}
]
},
@ -132,7 +131,7 @@
{
"sha256": "59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722",
"type": "archive",
"url": "https://dl.bintray.com/boostorg/release/1.72.0/source/boost_1_72_0.tar.bz2"
"url": "https://sourceforge.net/projects/boost/files/boost/1.72.0/boost_1_72_0.tar.bz2"
}
]
},
@ -147,9 +146,9 @@
"name": "mtxclient",
"sources": [
{
"sha256": "df3fe7e3d59b5fc52ee3ca9a132a55fc325aa799c676e9e420073c56daeb1848",
"type": "archive",
"url": "https://github.com/Nheko-Reborn/mtxclient/archive/5838f607d0e4c7595439249e8b9c213aec0667e9.tar.gz"
"commit": "ad5575bc24089dc385e97d9ace026414b618775c",
"type": "git",
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
}
]
},
@ -172,7 +171,8 @@
{
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DLMDBXX_INCLUDE_DIR=.deps/lmdbxx"
"-DLMDBXX_INCLUDE_DIR=.deps/lmdbxx",
"-DCOMPILE_QML=ON"
],
"buildsystem": "cmake-ninja",
"name": "nheko",

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
The below media files were obtained from https://github.com/matrix-org/matrix-react-sdk/tree/develop/res/media
callend.ogg
ringback.ogg
ring.ogg

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 mujx, nheko reborn developers -->
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2020 mujx, nheko reborn developers -->
<component type="desktop">
<id>nheko.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
@ -43,14 +43,25 @@
<url type="homepage">https://github.com/Nheko-Reborn/nheko</url>
<update_contact>https://github.com/Nheko-Reborn</update_contact>
<releases>
<release version="0.6.4" date="2019-05-22" />
<release version="0.6.3" date="2019-02-08" />
<release version="0.6.2" date="2018-10-07" />
<release version="0.6.1" date="2018-09-26" />
<release version="0.6.0" date="2018-09-21" />
<release version="0.5.5" date="2018-09-01" />
<release version="0.5.4" date="2018-08-21" />
<release version="0.5.3" date="2018-08-12" />
<release version="0.5.2" date="2018-07-28" />
<release date="2020-06-12" version="0.7.2"/>
<release date="2020-04-24" version="0.7.1"/>
<release date="2020-04-19" version="0.7.0"/>
<release date="2019-05-22" version="0.6.4"/>
<release date="2019-02-08" version="0.6.3"/>
<release date="2018-10-07" version="0.6.2"/>
<release date="2018-09-26" version="0.6.1"/>
<release date="2018-09-21" version="0.6.0"/>
<release date="2018-09-01" version="0.5.5"/>
<release date="2018-08-21" version="0.5.4"/>
<release date="2018-08-12" version="0.5.3"/>
<release date="2018-07-28" version="0.5.2"/>
</releases>
<developer_name>Nheko Reborn</developer_name>
<url type="bugtracker">https://github.com/Nheko-Reborn/nheko/issues</url>
<url type="help">https://github.com/Nheko-Reborn/nheko/</url>
<url type="translate">https://weblate.nheko.im/projects/nheko/</url>
</component>

@ -0,0 +1,113 @@
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import im.nheko 1.0
Rectangle {
id: activeCallBar
visible: TimelineManager.callState != WebRTCState.DISCONNECTED
color: "#2ECC71"
implicitHeight: rowLayout.height + 8
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
Avatar {
width: avatarSize
height: avatarSize
url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: TimelineManager.callPartyName
}
Label {
font.pointSize: fontMetrics.font.pointSize * 1.1
text: " " + TimelineManager.callPartyName + " "
}
Image {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
source: "qrc:/icons/icons/ui/place-call.png"
}
Label {
id: callStateLabel
font.pointSize: fontMetrics.font.pointSize * 1.1
}
Connections {
function onCallStateChanged(state) {
switch (state) {
case WebRTCState.INITIATING:
callStateLabel.text = qsTr("Initiating...");
break;
case WebRTCState.OFFERSENT:
callStateLabel.text = qsTr("Calling...");
break;
case WebRTCState.CONNECTING:
callStateLabel.text = qsTr("Connecting...");
break;
case WebRTCState.CONNECTED:
callStateLabel.text = "00:00";
var d = new Date();
callTimer.startTime = Math.floor(d.getTime() / 1000);
break;
case WebRTCState.DISCONNECTED:
callStateLabel.text = "";
}
}
target: TimelineManager
}
Timer {
id: callTimer
property int startTime
function pad(n) {
return (n < 10) ? ("0" + n) : n;
}
interval: 1000
running: TimelineManager.callState == WebRTCState.CONNECTED
repeat: true
onTriggered: {
var d = new Date();
let seconds = Math.floor(d.getTime() / 1000 - startTime);
let s = Math.floor(seconds % 60);
let m = Math.floor(seconds / 60) % 60;
let h = Math.floor(seconds / 3600);
callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s);
}
}
Item {
Layout.fillWidth: true
}
ImageButton {
width: 24
height: 24
buttonTextColor: "#000000"
image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
onClicked: TimelineManager.toggleMicMute()
}
Item {
implicitWidth: 16
}
}
}

@ -1,53 +1,75 @@
import QtQuick 2.6
import QtGraphicalEffects 1.0
import Qt.labs.settings 1.0
import QtQuick 2.6
import QtQuick.Controls 2.3
import im.nheko 1.0
Rectangle {
id: avatar
width: 48
height: 48
radius: settings.avatar_circles ? height/2 : 3
Settings {
id: settings
category: "user"
property bool avatar_circles: true
}
property alias url: img.source
property string userid
property string displayName
Text {
width: 48
height: 48
radius: Settings.avatarCircles ? height / 2 : 3
color: colors.base
Label {
anchors.fill: parent
text: chat.model.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0)))
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
textFormat: Text.RichText
color: colors.text
font.pixelSize: avatar.height/2
font.pixelSize: avatar.height / 2
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
visible: img.status != Image.Ready
color: colors.text
}
Image {
id: img
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
mipmap: true
smooth: false
sourceSize.width: avatar.width
sourceSize.height: avatar.height
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
anchors.fill: parent
width: avatar.width
height: avatar.height
radius: settings.avatar_circles ? height/2 : 3
radius: Settings.avatarCircles ? height / 2 : 3
}
}
}
Rectangle {
anchors.bottom: avatar.bottom
anchors.right: avatar.right
visible: !!userid
height: avatar.height / 6
width: height
radius: Settings.avatarCircles ? height / 2 : height / 4
color: {
switch (TimelineManager.userPresence(userid)) {
case "online":
return "#00cc66";
case "unavailable":
return "#ff9933";
case "offline":
default:
// return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled
"transparent";
}
color: colors.base
}
}
}

@ -4,23 +4,41 @@ import im.nheko 1.0
Rectangle {
id: indicator
property bool encrypted: false
function getEncryptionImage() {
if (encrypted)
return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText;
else
return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d";
}
function getEncryptionTooltip() {
if (encrypted)
return qsTr("Encrypted");
else
return qsTr("This message is not encrypted!");
}
color: "transparent"
width: 16
height: 16
ToolTip.visible: ma.containsMouse && indicator.visible
ToolTip.text: qsTr("Encrypted")
ToolTip.text: getEncryptionTooltip()
MouseArea{
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
}
Image {
id: stateImg
anchors.fill: parent
source: "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText
source: getEncryptionImage()
}
}
}

@ -1,29 +1,30 @@
import QtQuick 2.3
import QtQuick.Controls 2.3
Button {
property string image: undefined
AbstractButton {
id: button
flat: true
property string image: undefined
property color highlightColor: colors.highlight
property color buttonTextColor: colors.buttonText
// disable background, because we don't want a border on hover
background: Item {
}
width: 16
height: 16
Image {
id: buttonImg
// Workaround, can't get icon.source working for now...
anchors.fill: parent
source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText)
source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor)
}
MouseArea
{
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}
}

@ -1,32 +1,37 @@
import QtQuick 2.5
import QtQuick.Controls 2.3
import im.nheko 1.0
TextEdit {
textFormat: TextEdit.RichText
readOnly: true
wrapMode: Text.Wrap
selectByMouse: true
activeFocusOnPress: false
color: colors.text
onLinkActivated: {
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1])
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1])
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link)
timelineManager.setHistoryView(match[1])
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain)
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) {
chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]);
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) {
TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]);
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) {
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link);
TimelineManager.setHistoryView(match[1]);
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain);
} else {
TimelineManager.openLink(link);
}
else Qt.openUrlExternally(link)
}
MouseArea
{
ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink
MouseArea {
id: ma
anchors.fill: parent
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink
}

@ -0,0 +1,95 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import im.nheko 1.0
// This class is for showing Reactions in the timeline row, not for
// adding new reactions via the emoji picker
Flow {
id: reactionFlow
// highlight colors for selfReactedEvent background
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
property string eventId
property alias reactions: repeater.model
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
Repeater {
id: repeater
delegate: AbstractButton {
id: reaction
hoverEnabled: true
implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding * 2
implicitHeight: contentItem.childrenRect.height
ToolTip.visible: hovered
ToolTip.text: modelData.users
onClicked: {
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent);
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key);
}
contentItem: Row {
anchors.centerIn: parent
spacing: reactionText.implicitHeight / 4
leftPadding: reactionText.implicitHeight / 2
rightPadding: reactionText.implicitHeight / 2
TextMetrics {
id: textMetrics
font.family: Settings.emojiFont
elide: Text.ElideRight
elideWidth: 150
text: modelData.key
}
Text {
id: reactionText
anchors.baseline: reactionCounter.baseline
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…")
font.family: Settings.emojiFont
color: reaction.hovered ? colors.highlight : colors.text
maximumLineCount: 1
}
Rectangle {
id: divider
height: Math.floor(reactionCounter.implicitHeight * 1.4)
width: 1
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text
}
Text {
id: reactionCounter
anchors.verticalCenter: divider.verticalCenter
text: modelData.count
font: reaction.font
color: reaction.hovered ? colors.highlight : colors.text
}
}
background: Rectangle {
anchors.centerIn: parent
implicitWidth: reaction.implicitWidth
implicitHeight: reaction.implicitHeight
border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text
color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.base
border.width: 1
radius: reaction.height / 2
}
}
}
}

@ -0,0 +1,108 @@
/*
* Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net>
* Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*
* Shamelessly stolen from:
* https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml
*
* The MouseArea + interactive: false + maximumFlickVelocity are required
* to fix scrolling for desktop systems where we don't want flicking behaviour.
*
* See also:
* ScrollView.qml in qtquickcontrols
* qquickwheelarea.cpp in qtquickcontrols
*/
import QtQuick 2.9
import QtQuick.Controls 2.3
MouseArea {
// console.warn("Delta: ", wheel.pixelDelta.y);
// console.warn("Old position: ", flickable.contentY);
// console.warn("New position: ", newPos);
id: root
property Flickable flickable
property alias enabled: root.enabled
function calculateNewPosition(flickableItem, wheel) {
//Nothing to scroll
if (flickableItem.contentHeight < flickableItem.height)
return flickableItem.contentY;
//Ignore 0 events (happens at least with Christians trackpad)
if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0)
return flickableItem.contentY;
//pixelDelta seems to be the same as angleDelta/8
var pixelDelta = 0;
//The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta
if (wheel.angleDelta.y) {
var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property
var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit
var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps.
pixelDelta = ticks * pixelPerLine * wheelScrollLines;
} else {
pixelDelta = wheel.pixelDelta.y;
}
pixelDelta = Math.round(pixelDelta);
if (!pixelDelta)
return flickableItem.contentY;
var minYExtent = flickableItem.originY + flickableItem.topMargin;
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height;
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem)
minYExtent += flickableItem.headerItem.height;
//Avoid overscrolling
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta));
}
propagateComposedEvents: true
//Place the mouse area under the flickable
z: -1
onFlickableChanged: {
if (enabled) {
flickable.maximumFlickVelocity = 100000;
flickable.boundsBehavior = Flickable.StopAtBounds;
root.parent = flickable;
}
}
acceptedButtons: Qt.NoButton
onWheel: {
var newPos = calculateNewPosition(flickable, wheel);
// Show the scrollbars
flickable.flick(0, 0);
flickable.contentY = newPos;
cancelFlickStateTimer.start();
}
Timer {
id: cancelFlickStateTimer
//How long the scrollbar will remain visible
interval: 500
// Hide the scrollbars
onTriggered: {
flickable.cancelFlick();
flickable.movementEnded();
}
}
}

@ -4,36 +4,54 @@ import im.nheko 1.0
Rectangle {
id: indicator
property int state: 0
color: "transparent"
width: 16
height: 16
ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty
ToolTip.text: switch (state) {
case MtxEvent.Failed: return qsTr("Failed")
case MtxEvent.Sent: return qsTr("Sent")
case MtxEvent.Received: return qsTr("Received")
case MtxEvent.Read: return qsTr("Read")
default: return ""
ToolTip.text: {
switch (state) {
case MtxEvent.Failed:
return qsTr("Failed");
case MtxEvent.Sent:
return qsTr("Sent");
case MtxEvent.Received:
return qsTr("Received");
case MtxEvent.Read:
return qsTr("Read");
default:
return "";
}
MouseArea{
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
}
Image {
id: stateImg
// Workaround, can't get icon.source working for now...
anchors.fill: parent
source: switch (indicator.state) {
case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText
case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText
case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText
case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText
default: return ""
source: {
switch (indicator.state) {
case MtxEvent.Failed:
return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText;
case MtxEvent.Sent:
return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText;
case MtxEvent.Received:
return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText;
case MtxEvent.Read:
return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText;
default:
return "";
}
}
}
}
}

@ -1,52 +1,44 @@
import "./delegates"
import "./emoji"
import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import im.nheko 1.0
import "./delegates"
MouseArea {
id: rowArea
Item {
anchors.left: parent.left
anchors.right: parent.right
height: row.height
hoverEnabled: true
preventStealing: true
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
property bool showButtons: false
preventStealing: true
hoverEnabled: true
acceptedButtons: Qt.AllButtons
onClicked: {
if (mouse.button === Qt.RightButton)
messageContextMenu.show(model.id, model.type, model.isEncrypted, row);
Timer {
running: rowArea.containsMouse
interval: 150
onTriggered: rowArea.state = "showButtons"
}
onPressAndHold: {
messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y));
}
}
states: [
State {
name: "hideButtons"
when: !rowArea.containsMouse
PropertyChanges { target: rowArea; showButtons: false; }
},
State {
name: "showButtons"
PropertyChanges { target: rowArea; showButtons: true; }
Rectangle {
color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent"
anchors.fill: row
}
]
RowLayout {
id: row
anchors.leftMargin: avatarSize + 4
anchors.leftMargin: avatarSize + 16
anchors.left: parent.left
anchors.right: parent.right
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
@ -55,8 +47,8 @@ MouseArea {
// fancy reply, if this is a reply
Reply {
visible: model.replyTo
modelData: chat.model.getDump(model.replyTo)
userColor: timelineManager.userColor(modelData.userId, colors.window)
modelData: chat.model.getDump(model.replyTo, model.id)
userColor: TimelineManager.userColor(modelData.userId, colors.window)
}
// actual message content
@ -64,71 +56,93 @@ MouseArea {
id: contentItem
width: parent.width
modelData: model
}
Reactions {
id: reactionRow
reactions: model.reactions
eventId: model.id
}
ImageButton {
visible: rowArea.showButtons
}
StatusIndicator {
state: model.state
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
id: replyButton
hoverEnabled: true
}
image: ":/icons/icons/ui/mail-reply.png"
EncryptionIndicator {
visible: model.isRoomEncrypted
encrypted: model.isEncrypted
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
}
ToolTip.visible: hovered
ToolTip.text: qsTr("Reply")
EmojiButton {
id: reactButton
onClicked: chat.model.replyAction(model.id)
}
ImageButton {
visible: rowArea.showButtons
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
id: optionsButton
hoverEnabled: true
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Options")
onClicked: messageContextMenu.show(model.id, model.type, optionsButton)
ToolTip.text: qsTr("React")
emojiPicker: emojiPopup
event_id: model.id
}
StatusIndicator {
state: model.state
ImageButton {
id: replyButton
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/mail-reply.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Reply")
onClicked: chat.model.replyAction(model.id)
}
EncryptionIndicator {
visible: model.isEncrypted
ImageButton {
id: optionsButton
visible: Settings.buttonsInTimeline
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredHeight: 16
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Options")
onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton)
}
Text {
Label {
Layout.alignment: Qt.AlignRight | Qt.AlignTop
text: model.timestamp.toLocaleTimeString("HH:mm")
width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
color: inactiveColors.text
ToolTip.visible: ma.containsMouse
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
MouseArea{
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
ToolTip.visible: ma.containsMouse
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
}
}
}

@ -1,166 +1,350 @@
import "./delegates"
import "./device-verification"
import "./emoji"
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
import QtQuick.Window 2.2
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
import "./delegates"
Page {
id: timelineRoot
Item {
property var colors: currentActivePalette
property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled }
property var systemInactive
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
property int avatarSize: 40
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
Menu {
id: messageContextMenu
palette: colors
modal: true
function show(eventId_, eventType_, showAt) {
eventId = eventId_
eventType = eventType_
popup(showAt)
FontMetrics {
id: fontMetrics
}
EmojiPicker {
id: emojiPopup
width: 7 * 52 + 20
height: 6 * 52
colors: palette
model: EmojiProxyModel {
category: EmojiCategory.People
sourceModel: EmojiModel {
}
}
}
Menu {
id: messageContextMenu
property string eventId
property int eventType
property bool isEncrypted
function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
if (position)
popup(position, showAt_);
else
popup(showAt_);
}
modal: true
MenuItem {
text: qsTr("React")
onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Reply")
onClicked: chat.model.replyAction(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Read receipts")
onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Mark as read")
}
MenuItem {
text: qsTr("View raw message")
onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
}
MenuItem {
visible: messageContextMenu.isEncrypted
height: visible ? implicitHeight : 0
text: qsTr("View decrypted raw message")
onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId)
}
MenuItem {
text: qsTr("Redact message")
onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
}
MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
height: visible ? implicitHeight : 0
text: qsTr("Save as")
onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
id: timelineRoot
}
Rectangle {
anchors.fill: parent
color: colors.window
Text {
visible: !timelineManager.timeline && !timelineManager.isInitialSync
Component {
id: deviceVerificationDialog
DeviceVerification {
}
}
Connections {
function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) {
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
"flow": flow
});
dialog.show();
}
target: TimelineManager
}
Connections {
function onOpenProfile(profile) {
var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile
});
userProfile.show();
}
target: TimelineManager.timeline
}
Label {
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24
color: colors.windowText
color: colors.text
}
BusyIndicator {
visible: running
anchors.centerIn: parent
running: timelineManager.isInitialSync
running: TimelineManager.isInitialSync
height: 200
width: 200
z: 3
}
ListView {
id: chat
ColumnLayout {
anchors.fill: parent
visible: timelineManager.timeline != null
Rectangle {
id: topBar
Layout.fillWidth: true
implicitHeight: topLayout.height + 16
z: 3
color: colors.base
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
GridLayout {
//Layout.margins: 8
id: topLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: chatFooter.top
anchors.margins: 8
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 4
anchors.rightMargin: scrollbar.width
ImageButton {
id: backToRoomsButton
model: timelineManager.timeline
Layout.column: 0
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
visible: TimelineManager.isNarrowView
image: ":/icons/icons/ui/angle-pointing-to-left.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Back to room list")
onClicked: TimelineManager.backToRooms()
}
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
Avatar {
Layout.column: 1
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
width: avatarSize
height: avatarSize
url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
displayName: chat.model ? chat.model.roomName : qsTr("No room selected")
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
onWheel: {
if (wheel.angleDelta != 0) {
chat.contentY = chat.contentY - wheel.angleDelta.y
wheel.accepted = true
chat.returnToBounds()
onClicked: TimelineManager.openRoomSettings()
}
}
Label {
Layout.fillWidth: true
Layout.column: 2
Layout.row: 0
color: colors.text
font.pointSize: fontMetrics.font.pointSize * 1.1
text: chat.model ? chat.model.roomName : qsTr("No room selected")
MouseArea {
anchors.fill: parent
onClicked: TimelineManager.openRoomSettings()
}
Shortcut {
sequence: StandardKey.MoveToPreviousPage
onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); }
}
Shortcut {
sequence: StandardKey.MoveToNextPage
onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); }
MatrixText {
Layout.fillWidth: true
Layout.column: 2
Layout.row: 1
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
clip: true
text: chat.model ? chat.model.roomTopic : ""
}
ImageButton {
id: roomOptionsButton
Layout.column: 3
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
image: ":/icons/icons/ui/vertical-ellipsis.png"
ToolTip.visible: hovered
ToolTip.text: qsTr("Room options")
onClicked: roomOptionsMenu.popup(roomOptionsButton)
Menu {
id: roomOptionsMenu
MenuItem {
text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsersDialog()
}
MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openMemberListDialog()
}
MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog()
}
MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings()
}
}
}
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
parent: chat.parent
anchors.top: chat.top
anchors.left: chat.right
anchors.bottom: chat.bottom
}
ListView {
id: chat
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
visible: TimelineManager.timeline != null
cacheBuffer: 400
Layout.fillWidth: true
Layout.fillHeight: true
model: TimelineManager.timeline
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
spacing: 4
verticalLayoutDirection: ListView.BottomToTop
onCountChanged: {
if (atYEnd)
model.currentIndex = 0;
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom
} // Mark last event as read, since we are at the bottom
delegate: Rectangle {
// This would normally be previousSection, but our model's order is inverted.
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
ScrollHelper {
flickable: parent
anchors.fill: parent
}
id: wrapper
property Item section
width: chat.width
height: section ? section.height + timelinerow.height : timelinerow.height
color: "transparent"
Shortcut {
sequence: StandardKey.MoveToPreviousPage
onActivated: {
chat.contentY = chat.contentY - chat.height / 2;
chat.returnToBounds();
}
}
TimelineRow {
id: timelinerow
y: section ? section.y + section.height : 0
Shortcut {
sequence: StandardKey.MoveToNextPage
onActivated: {
chat.contentY = chat.contentY + chat.height / 2;
chat.returnToBounds();
}
}
onSectionBoundaryChanged: {
if (sectionBoundary) {
var properties = {
'modelData': model.dump,
'section': ListView.section,
'nextSection': ListView.nextSection
Shortcut {
sequence: StandardKey.Cancel
onActivated: chat.model.reply = undefined
}
section = sectionHeader.createObject(wrapper, properties)
} else {
section.destroy()
section = null
Shortcut {
sequence: "Alt+Up"
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
}
Shortcut {
sequence: "Alt+Down"
onActivated: {
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
}
}
Component {
id: userProfileComponent
Binding {
target: chat.model
property: "currentIndex"
when: y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height
value: index
delayed: true
UserProfile {
}
}
@ -168,8 +352,10 @@ Item {
section {
property: "section"
}
Component {
id: sectionHeader
Column {
property var modelData
property string section
@ -178,101 +364,174 @@ Item {
topPadding: 4
bottomPadding: 4
spacing: 8
visible: !!modelData
width: parent.width
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: section.includes(" ")
text: chat.model.formatDateSeparator(modelData.timestamp)
color: colors.windowText
height: contentHeight * 1.2
color: colors.text
height: fontMetrics.height * 1.4
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
background: Rectangle {
radius: parent.height / 2
color: colors.base
}
}
Row {
height: userName.height
spacing: 4
spacing: 8
Avatar {
width: avatarSize
height: avatarSize
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
displayName: modelData.userName
userid: modelData.userId
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(modelData.userId)
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
Text {
Label {
id: userName
text: chat.model.escapeEmoji(modelData.userName)
color: timelineManager.userColor(modelData.userId, colors.window)
text: TimelineManager.escapeEmoji(modelData.userName)
color: TimelineManager.userColor(modelData.userId, colors.window)
textFormat: Text.RichText
MouseArea {
anchors.fill: parent
onClicked: chat.model.openUserProfile(section.split(" ")[0])
Layout.alignment: Qt.AlignHCenter
onClicked: chat.model.openUserProfile(modelData.userId)
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
id: scrollbar
}
Rectangle {
delegate: Item {
id: wrapper
// This would normally be previousSection, but our model's order is inverted.
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
property Item section
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
width: chat.delegateMaxWidth
height: section ? section.height + timelinerow.height : timelinerow.height
onSectionBoundaryChanged: {
if (sectionBoundary) {
var properties = {
"modelData": model.dump,
"section": ListView.section,
"nextSection": ListView.nextSection
};
section = sectionHeader.createObject(wrapper, properties);
} else {
section.destroy();
section = null;
}
}
TimelineRow {
id: timelinerow
y: section ? section.y + section.height : 0
}
Connections {
function onMovementEnded() {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
}
target: chat
}
}
footer: BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: chat.model && chat.model.paginationInProgress
height: 50
width: 50
z: 3
}
}
Item {
id: chatFooter
height: Math.max(16, footerContent.height)
implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height)
Layout.fillWidth: true
z: 3
Column {
id: footerContent
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 3
color: "transparent"
Rectangle {
id: typingRect
Column {
id: footerContent
anchors.left: parent.left
anchors.right: parent.right
color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent"
height: typingDisplay.height
Text {
Label {
id: typingDisplay
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
color: colors.text
text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : ""
textFormat: Text.RichText
color: colors.windowText
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
}
Rectangle {
id: replyPopup
visible: timelineManager.replyingEvent && chat.model
anchors.left: parent.left
anchors.right: parent.right
visible: chat.model && chat.model.reply
// Height of child, plus margins, plus border
height: replyPreview.height + 10
color: colors.base
Reply {
id: replyPreview
@ -281,9 +540,9 @@ Item {
anchors.right: closeReplyButton.left
anchors.rightMargin: 20
anchors.bottom: parent.bottom
modelData: chat.model ? chat.model.getDump(timelineManager.replyingEvent) : {}
userColor: timelineManager.userColor(modelData.userId, colors.window)
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {
}
userColor: TimelineManager.userColor(modelData.userId, colors.window)
}
ImageButton {
@ -295,15 +554,29 @@ Item {
hoverEnabled: true
width: 16
height: 16
image: ":/icons/icons/ui/remove-symbol.png"
ToolTip.visible: closeReplyButton.hovered
ToolTip.text: qsTr("Close")
onClicked: chat.model.reply = undefined
}
onClicked: timelineManager.closeReply()
}
}
}
ActiveCallBar {
Layout.fillWidth: true
z: 3
}
}
}
systemInactive: SystemPalette {
colorGroup: SystemPalette.Disabled
}
}

@ -0,0 +1,175 @@
import "./device-verification"
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.3
import im.nheko 1.0
ApplicationWindow {
id: userProfileDialog
property var profile
height: 650
width: 420
minimumHeight: 420
palette: colors
Component {
id: deviceVerificationDialog
DeviceVerification {
}
}
ColumnLayout {
id: contentL
anchors.fill: parent
anchors.margins: 10
spacing: 10
Avatar {
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/")
height: 130
width: 130
displayName: profile.displayName
userid: profile.userid
Layout.alignment: Qt.AlignHCenter
}
Label {
text: profile.displayName
fontSizeMode: Text.HorizontalFit
font.pixelSize: 20
color: TimelineManager.userColor(profile.userid, colors.window)
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
MatrixText {
text: profile.userid
font.pixelSize: 15
Layout.alignment: Qt.AlignHCenter
}
Button {
id: verifyUserButton
text: qsTr("Verify")
Layout.alignment: Qt.AlignHCenter
enabled: !profile.isUserVerified
visible: !profile.isUserVerified
onClicked: profile.verify()
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 8
ImageButton {
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Ban the user")
onClicked: profile.banUser()
}
// ImageButton{
// image:":/icons/icons/ui/volume-off-indicator.png"
// Layout.margins: {
// left: 5
// right: 5
// }
// ToolTip.visible: hovered
// ToolTip.text: qsTr("Ignore messages from this user")
// onClicked : {
// profile.ignoreUser()
// }
// }
ImageButton {
image: ":/icons/icons/ui/black-bubble-speech.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Start a private chat")
onClicked: profile.startChat()
}
ImageButton {
image: ":/icons/icons/ui/round-remove-button.png"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Kick the user")
onClicked: profile.kickUser()
}
}
ListView {
id: devicelist
Layout.fillHeight: true
Layout.minimumHeight: 200
Layout.fillWidth: true
clip: true
spacing: 8
boundsBehavior: Flickable.StopAtBounds
model: profile.deviceList
delegate: RowLayout {
width: devicelist.width
spacing: 4
ColumnLayout {
spacing: 0
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight
font.bold: true
color: colors.text
text: model.deviceId
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
elide: Text.ElideRight
color: colors.text
text: model.deviceName
}
}
Image {
Layout.preferredHeight: 16
Layout.preferredWidth: 16
source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red"))
}
Button {
id: verifyButton
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify"
onClicked: {
if (model.verificationStatus == VerificationStatus.VERIFIED)
profile.unverify(model.deviceId);
else
profile.verify(model.deviceId);
}
}
}
}
}
footer: DialogButtonBox {
standardButtons: DialogButtonBox.Ok
onAccepted: userProfileDialog.close()
}
}

@ -1,9 +1,8 @@
import QtQuick 2.6
import QtQuick.Layouts 1.2
import im.nheko 1.0
Rectangle {
radius: 10
color: colors.base
Item {
height: row.height + 24
width: parent ? parent.width : undefined
@ -12,46 +11,65 @@ Rectangle {
anchors.centerIn: parent
width: parent.width - 24
spacing: 15
Rectangle {
id: button
color: colors.light
radius: 22
height: 44
width: 44
Image {
id: img
anchors.centerIn: parent
anchors.centerIn: parent
source: "qrc:/icons/icons/ui/arrow-pointing-down.png"
fillMode: Image.Pad
}
MouseArea {
anchors.fill: parent
onClicked: timelineManager.timeline.saveMedia(model.data.id)
onClicked: TimelineManager.timeline.saveMedia(model.data.id)
cursorShape: Qt.PointingHandCursor
}
}
ColumnLayout {
id: col
Text {
id: filename
Layout.fillWidth: true
text: model.data.body
text: model.data.filename
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
Text {
id: filesize
Layout.fillWidth: true
text: model.data.filesize
textFormat: Text.PlainText
elide: Text.ElideRight
color: colors.text
}
}
}
Rectangle {
color: colors.dark
z: -1
radius: 10
height: row.height + 24
width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth))
}
}

@ -1,28 +1,70 @@
import QtQuick 2.6
import im.nheko 1.0
Item {
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width)
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width)
property double tempHeight: tempWidth * model.data.proportionalHeight
property double divisor: model.isReply ? 4 : 2
property bool tooHigh: tempHeight > timelineRoot.height / divisor
height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight)
width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth)
property bool tooHigh: tempHeight > timelineRoot.height / 2
Image {
id: blurhash
height: tooHigh ? timelineRoot.height / 2 : tempHeight
width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth
anchors.fill: parent
visible: img.status != Image.Ready
source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText)
asynchronous: true
fillMode: Image.PreserveAspectFit
sourceSize.width: parent.width
sourceSize.height: parent.height
}
Image {
id: img
anchors.fill: parent
anchors.fill: parent
source: model.data.url.replace("mxc://", "image://MxcImage/")
asynchronous: true
fillMode: Image.PreserveAspectFit
MouseArea {
enabled: model.data.type == MtxEvent.ImageMessage
id: mouseArea
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
hoverEnabled: true
anchors.fill: parent
onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id)
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id)
}
Item {
id: overlay
anchors.fill: parent
visible: mouseArea.containsMouse
Rectangle {
id: container
width: parent.width
implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom
color: colors.window
opacity: 0.75
}
Text {
id: imgcaption
anchors.fill: container
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: model.data.filename ? model.data.filename : model.data.body
color: colors.text
}
}
}
}

@ -2,124 +2,334 @@ import QtQuick 2.6
import im.nheko 1.0
Item {
property alias modelData: model.data
property alias isReply: model.isReply
property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
height: chooser.childrenRect.height
// Workaround to have an assignable global property
Item {
id: model
property var data;
}
property alias modelData: model.data
height: chooser.childrenRect.height
property var data
property bool isReply: false
}
DelegateChooser {
id: chooser
//role: "type" //< not supported in our custom implementation, have to use roleValue
roleValue: model.data.type
anchors.fill: parent
DelegateChoice {
roleValue: MtxEvent.UnknownMessage
Placeholder { text: "Unretrieved event" }
Placeholder {
text: "Unretrieved event"
}
}
DelegateChoice {
roleValue: MtxEvent.TextMessage
TextMessage {}
TextMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.NoticeMessage
NoticeMessage {}
NoticeMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.EmoteMessage
NoticeMessage {
formatted: chat.model.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
color: timelineManager.userColor(modelData.userId, colors.window)
formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
color: TimelineManager.userColor(modelData.userId, colors.window)
}
}
DelegateChoice {
roleValue: MtxEvent.ImageMessage
ImageMessage {}
ImageMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.Sticker
ImageMessage {}
ImageMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.FileMessage
FileMessage {}
FileMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.VideoMessage
PlayableMediaMessage {}
PlayableMediaMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.AudioMessage
PlayableMediaMessage {}
PlayableMediaMessage {
}
}
DelegateChoice {
roleValue: MtxEvent.Redacted
Pill {
text: qsTr("redacted")
}
}
DelegateChoice {
roleValue: MtxEvent.Redaction
Pill {
text: qsTr("redacted")
}
}
DelegateChoice {
roleValue: MtxEvent.Encryption
Pill {
text: qsTr("Encryption enabled")
}
}
DelegateChoice {
roleValue: MtxEvent.Name
NoticeMessage {
text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
}
}
DelegateChoice {
roleValue: MtxEvent.Topic
NoticeMessage {
text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
}
}
DelegateChoice {
roleValue: MtxEvent.RoomCreate
NoticeMessage {
text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId)
}
}
DelegateChoice {
roleValue: MtxEvent.CallInvite
NoticeMessage {
text: {
switch (model.data.callType) {
case "voice":
return qsTr("%1 placed a voice call.").arg(model.data.userName);
case "video":
return qsTr("%1 placed a video call.").arg(model.data.userName);
default:
return qsTr("%1 placed a call.").arg(model.data.userName);
}
}
}
}
DelegateChoice {
roleValue: MtxEvent.CallAnswer
NoticeMessage {
text: qsTr("%1 answered the call.").arg(model.data.userName)
}
}
DelegateChoice {
roleValue: MtxEvent.CallHangUp
NoticeMessage {
text: qsTr("%1 ended the call.").arg(model.data.userName)
}
}
DelegateChoice {
roleValue: MtxEvent.CallCandidates
NoticeMessage {
text: qsTr("Negotiating call...")
}
}
DelegateChoice {
// TODO: make a more complex formatter for the power levels.
roleValue: MtxEvent.PowerLevels
NoticeMessage {
text: timelineManager.timeline.formatPowerLevelEvent(model.data.id)
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.RoomJoinRules
NoticeMessage {
text: timelineManager.timeline.formatJoinRuleEvent(model.data.id)
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.RoomHistoryVisibility
NoticeMessage {
text: timelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.RoomGuestAccess
NoticeMessage {
text: timelineManager.timeline.formatGuestAccessEvent(model.data.id)
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.Member
NoticeMessage {
text: TimelineManager.timeline.formatMemberEvent(model.data.id)
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationRequest
NoticeMessage {
text: "KeyVerificationRequest"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationStart
NoticeMessage {
text: "KeyVerificationStart"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationReady
NoticeMessage {
text: "KeyVerificationReady"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationCancel
NoticeMessage {
text: timelineManager.timeline.formatMemberEvent(model.data.id);
text: "KeyVerificationCancel"
}
}
DelegateChoice {
Placeholder {}
roleValue: MtxEvent.KeyVerificationKey
NoticeMessage {
text: "KeyVerificationKey"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationMac
NoticeMessage {
text: "KeyVerificationMac"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationDone
NoticeMessage {
text: "KeyVerificationDone"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationDone
NoticeMessage {
text: "KeyVerificationDone"
}
}
DelegateChoice {
roleValue: MtxEvent.KeyVerificationAccept
NoticeMessage {
text: "KeyVerificationAccept"
}
}
DelegateChoice {
Placeholder {
}
}
}
}

@ -1,4 +1,6 @@
TextMessage {
font.italic: true
color: inactiveColors.text
color: colors.buttonText
height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined
clip: true
}

@ -2,13 +2,14 @@ import QtQuick 2.5
import QtQuick.Controls 2.1
Label {
color: inactiveColors.text
color: colors.brightText
horizontalAlignment: Text.AlignHCenter
height: contentHeight * 1.2
width: contentWidth * 1.2
background: Rectangle {
radius: parent.height / 2
color: colors.base
color: colors.dark
}
}

@ -1,27 +1,35 @@
import QtMultimedia 5.6
import QtQuick 2.6
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.1
import QtMultimedia 5.6
import QtQuick.Layouts 1.2
import im.nheko 1.0
Rectangle {
id: bg
radius: 10
color: colors.base
height: content.height + 24
color: colors.dark
height: Math.round(content.height + 24)
width: parent ? parent.width : undefined
Column {
id: content
width: parent.width - 24
anchors.centerIn: parent
Rectangle {
id: videoContainer
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width)
property double tempHeight: tempWidth * model.data.proportionalHeight
property double divisor: model.isReply ? 4 : 2
property bool tooHigh: tempHeight > timelineRoot.height / divisor
visible: model.data.type == MtxEvent.VideoMessage
width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size...
height: width*model.data.proportionalHeight
height: tooHigh ? timelineRoot.height / divisor : tempHeight
width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth
Image {
anchors.fill: parent
source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/")
@ -33,116 +41,153 @@ Rectangle {
fillMode: VideoOutput.PreserveAspectFit
source: media
}
}
}
RowLayout {
width: parent.width
Text {
id: positionText
text: "--:--:--"
color: colors.text
}
Slider {
Layout.fillWidth: true
id: progress
value: media.position
from: 0
to: media.duration
onMoved: media.seek(value)
//indeterminate: true
function updatePositionTexts() {
function formatTime(date) {
var hh = date.getUTCHours();
var mm = date.getUTCMinutes();
var ss = date.getSeconds();
if (hh < 10) {hh = "0"+hh;}
if (mm < 10) {mm = "0"+mm;}
if (ss < 10) {ss = "0"+ss;}
return hh+":"+mm+":"+ss;
if (hh < 10)
hh = "0" + hh;
if (mm < 10)
mm = "0" + mm;
if (ss < 10)
ss = "0" + ss;
return hh + ":" + mm + ":" + ss;
}
positionText.text = formatTime(new Date(media.position))
durationText.text = formatTime(new Date(media.duration))
positionText.text = formatTime(new Date(media.position));
durationText.text = formatTime(new Date(media.duration));
}
onValueChanged: updatePositionTexts()
Layout.fillWidth: true
value: media.position
from: 0
to: media.duration
onMoved: media.seek(value)
onValueChanged: updatePositionTexts()
palette: colors
}
Text {
id: durationText
text: "--:--:--"
color: colors.text
}
}
RowLayout {
width: parent.width
spacing: 15
Rectangle {
id: button
color: colors.window
radius: 22
height: 44
width: 44
states: [
State {
name: "stopped"
PropertyChanges {
target: img
source: "image://colorimage/:/icons/icons/ui/play-sign.png?" + colors.text
}
},
State {
name: "playing"
PropertyChanges {
target: img
source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + colors.text
}
}
]
Image {
id: img
anchors.centerIn: parent
z: 3
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text
fillMode: Image.Pad
}
MouseArea {
anchors.fill: parent
onClicked: {
switch (button.state) {
case "": timelineManager.timeline.cacheMedia(model.data.id); break;
case "":
TimelineManager.timeline.cacheMedia(model.data.id);
break;
case "stopped":
media.play(); console.log("play");
button.state = "playing"
break
media.play();
console.log("play");
button.state = "playing";
break;
case "playing":
media.pause(); console.log("pause");
button.state = "stopped"
break
media.pause();
console.log("pause");
button.state = "stopped";
break;
}
}
cursorShape: Qt.PointingHandCursor
}
MediaPlayer {
id: media
onError: console.log(errorString)
onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts()
onStatusChanged: {
if (status == MediaPlayer.Loaded)
progress.updatePositionTexts();
}
onStopped: button.state = "stopped"
}
Connections {
target: timelineManager.timeline
target: TimelineManager.timeline
onMediaCached: {
if (mxcUrl == model.data.url) {
media.source = "file://" + cacheUrl
button.state = "stopped"
console.log("media loaded: " + mxcUrl + " at " + cacheUrl)
media.source = "file://" + cacheUrl;
button.state = "stopped";
console.log("media loaded: " + mxcUrl + " at " + cacheUrl);
}
console.log("media cached: " + mxcUrl + " at " + cacheUrl)
console.log("media cached: " + mxcUrl + " at " + cacheUrl);
}
}
states: [
State {
name: "stopped"
PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text }
},
State {
name: "playing"
PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text }
}
]
}
ColumnLayout {
id: col
@ -153,6 +198,7 @@ Rectangle {
elide: Text.ElideRight
color: colors.text
}
Text {
Layout.fillWidth: true
text: model.data.filesize
@ -160,8 +206,11 @@ Rectangle {
elide: Text.ElideRight
color: colors.text
}
}
}
}
}
}

@ -2,8 +2,9 @@ import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import im.nheko 1.0
Rectangle {
Item {
id: replyComponent
property alias modelData: reply.modelData
@ -15,7 +16,7 @@ Rectangle {
MouseArea {
anchors.fill: parent
preventStealing: true
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(timelineManager.replyingEvent), ListView.Contain)
onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
cursorShape: Qt.PointingHandCursor
}
@ -25,19 +26,20 @@ Rectangle {
anchors.top: replyContainer.top
anchors.bottom: replyContainer.bottom
width: 4
color: timelineManager.userColor(reply.modelData.userId, colors.window)
color: TimelineManager.userColor(reply.modelData.userId, colors.window)
}
Column {
id: replyContainer
anchors.left: colorLine.right
anchors.leftMargin: 4
width: parent.width - 8
Text {
id: userName
text: chat.model ? chat.model.escapeEmoji(reply.modelData.userName) : ""
text: TimelineManager.escapeEmoji(reply.modelData.userName)
color: replyComponent.userColor
textFormat: Text.RichText
@ -46,13 +48,25 @@ Rectangle {
onClicked: chat.model.openUserProfile(reply.modelData.userId)
cursorShape: Qt.PointingHandCursor
}
}
MessageDelegate {
id: reply
width: parent.width
isReply: true
}
}
Rectangle {
id: backgroundItem
z: -1
height: replyContainer.height
width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width)
color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2)
}
}

@ -1,7 +1,12 @@
import ".."
import im.nheko 1.0
MatrixText {
property string formatted: model.data.formattedBody
text: "<style type=\"text/css\">a { color:"+colors.link+";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>")
text: "<style type=\"text/css\">a { color:" + colors.link + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>")
width: parent ? parent.width : undefined
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
clip: true
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
}

@ -0,0 +1,46 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Awaiting Confirmation")
ColumnLayout {
spacing: 16
Label {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Waiting for other side to complete verification.")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("Cancel")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
}
}
}

@ -0,0 +1,144 @@
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Window 2.10
import im.nheko 1.0
ApplicationWindow {
id: dialog
property var flow
onClosing: TimelineManager.removeVerificationFlow(flow)
title: stack.currentItem.title
flags: Qt.Dialog
palette: colors
height: stack.implicitHeight
width: stack.implicitWidth
StackView {
id: stack
initialItem: newVerificationRequest
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
}
Component {
id: newVerificationRequest
NewVerificationRequest {
}
}
Component {
id: waiting
Waiting {
}
}
Component {
id: success
Success {
}
}
Component {
id: failed
Failed {
}
}
Component {
id: digitVerification
DigitVerification {
}
}
Component {
id: emojiVerification
EmojiVerification {
}
}
Item {
state: flow.state
states: [
State {
name: "PromptStartVerification"
StateChangeScript {
script: stack.replace(newVerificationRequest)
}
},
State {
name: "CompareEmoji"
StateChangeScript {
script: stack.replace(emojiVerification)
}
},
State {
name: "CompareNumber"
StateChangeScript {
script: stack.replace(digitVerification)
}
},
State {
name: "WaitingForKeys"
StateChangeScript {
script: stack.replace(waiting)
}
},
State {
name: "WaitingForOtherToAccept"
StateChangeScript {
script: stack.replace(waiting)
}
},
State {
name: "WaitingForMac"
StateChangeScript {
script: stack.replace(waiting)
}
},
State {
name: "Success"
StateChangeScript {
script: stack.replace(success)
}
},
State {
name: "Failed"
StateChangeScript {
script: stack.replace(failed)
}
}
]
}
}

@ -0,0 +1,69 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Verification Code")
ColumnLayout {
spacing: 16
Label {
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[0]
color: colors.text
}
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[1]
color: colors.text
}
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[2]
color: colors.text
}
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("They do not match!")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("They match!")
onClicked: flow.next()
}
}
}
}

@ -0,0 +1,33 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
Rectangle {
color: "red"
implicitHeight: Qt.application.font.pixelSize * 4
implicitWidth: col.width
height: Qt.application.font.pixelSize * 4
width: col.width
ColumnLayout {
id: col
property var emoji: emojis.mapping[Math.floor(Math.random() * 64)]
anchors.bottom: parent.bottom
Label {
height: font.pixelSize * 2
Layout.alignment: Qt.AlignHCenter
text: col.emoji.emoji
font.pixelSize: Qt.application.font.pixelSize * 2
}
Label {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
text: col.emoji.description
}
}
}

@ -0,0 +1,414 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Verification Code")
ColumnLayout {
spacing: 16
Label {
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
id: emojis
property var mapping: [{
"number": 0,
"emoji": "🐶",
"description": "Dog",
"unicode": "U+1F436"
}, {
"number": 1,
"emoji": "🐱",
"description": "Cat",
"unicode": "U+1F431"
}, {
"number": 2,
"emoji": "🦁",
"description": "Lion",
"unicode": "U+1F981"
}, {
"number": 3,
"emoji": "🐎",
"description": "Horse",
"unicode": "U+1F40E"
}, {
"number": 4,
"emoji": "🦄",
"description": "Unicorn",
"unicode": "U+1F984"
}, {
"number": 5,
"emoji": "🐷",
"description": "Pig",
"unicode": "U+1F437"
}, {
"number": 6,
"emoji": "🐘",
"description": "Elephant",
"unicode": "U+1F418"
}, {
"number": 7,
"emoji": "🐰",
"description": "Rabbit",
"unicode": "U+1F430"
}, {
"number": 8,
"emoji": "🐼",
"description": "Panda",
"unicode": "U+1F43C"
}, {
"number": 9,
"emoji": "🐓",
"description": "Rooster",
"unicode": "U+1F413"
}, {
"number": 10,
"emoji": "🐧",
"description": "Penguin",
"unicode": "U+1F427"
}, {
"number": 11,
"emoji": "🐢",
"description": "Turtle",
"unicode": "U+1F422"
}, {
"number": 12,
"emoji": "🐟",
"description": "Fish",
"unicode": "U+1F41F"
}, {
"number": 13,
"emoji": "🐙",
"description": "Octopus",
"unicode": "U+1F419"
}, {
"number": 14,
"emoji": "🦋",
"description": "Butterfly",
"unicode": "U+1F98B"
}, {
"number": 15,
"emoji": "🌷",
"description": "Flower",
"unicode": "U+1F337"
}, {
"number": 16,
"emoji": "🌳",
"description": "Tree",
"unicode": "U+1F333"
}, {
"number": 17,
"emoji": "🌵",
"description": "Cactus",
"unicode": "U+1F335"
}, {
"number": 18,
"emoji": "🍄",
"description": "Mushroom",
"unicode": "U+1F344"
}, {
"number": 19,
"emoji": "🌏",
"description": "Globe",
"unicode": "U+1F30F"
}, {
"number": 20,
"emoji": "🌙",
"description": "Moon",
"unicode": "U+1F319"
}, {
"number": 21,
"emoji": "☁",
"description": "Cloud",
"unicode": "U+2601U+FE0F"
}, {
"number": 22,
"emoji": "🔥",
"description": "Fire",
"unicode": "U+1F525"
}, {
"number": 23,
"emoji": "🍌",
"description": "Banana",
"unicode": "U+1F34C"
}, {
"number": 24,
"emoji": "🍎",
"description": "Apple",
"unicode": "U+1F34E"
}, {
"number": 25,
"emoji": "🍓",
"description": "Strawberry",
"unicode": "U+1F353"
}, {
"number": 26,
"emoji": "🌽",
"description": "Corn",
"unicode": "U+1F33D"
}, {
"number": 27,
"emoji": "🍕",
"description": "Pizza",
"unicode": "U+1F355"
}, {
"number": 28,
"emoji": "🎂",
"description": "Cake",
"unicode": "U+1F382"
}, {
"number": 29,
"emoji": "❤",
"description": "Heart",
"unicode": "U+2764U+FE0F"
}, {
"number": 30,
"emoji": "😀",
"description": "Smiley",
"unicode": "U+1F600"
}, {
"number": 31,
"emoji": "🤖",
"description": "Robot",
"unicode": "U+1F916"
}, {
"number": 32,
"emoji": "🎩",
"description": "Hat",
"unicode": "U+1F3A9"
}, {
"number": 33,
"emoji": "👓",
"description": "Glasses",
"unicode": "U+1F453"
}, {
"number": 34,
"emoji": "🔧",
"description": "Spanner",
"unicode": "U+1F527"
}, {
"number": 35,
"emoji": "🎅",
"description": "Santa",
"unicode": "U+1F385"
}, {
"number": 36,
"emoji": "👍",
"description": "Thumbs Up",
"unicode": "U+1F44D"
}, {
"number": 37,
"emoji": "☂",
"description": "Umbrella",
"unicode": "U+2602U+FE0F"
}, {
"number": 38,
"emoji": "⌛",
"description": "Hourglass",
"unicode": "U+231B"
}, {
"number": 39,
"emoji": "⏰",
"description": "Clock",
"unicode": "U+23F0"
}, {
"number": 40,
"emoji": "🎁",
"description": "Gift",
"unicode": "U+1F381"
}, {
"number": 41,
"emoji": "💡",
"description": "Light Bulb",
"unicode": "U+1F4A1"
}, {
"number": 42,
"emoji": "📕",
"description": "Book",
"unicode": "U+1F4D5"
}, {
"number": 43,
"emoji": "✏",
"description": "Pencil",
"unicode": "U+270FU+FE0F"
}, {
"number": 44,
"emoji": "📎",
"description": "Paperclip",
"unicode": "U+1F4CE"
}, {
"number": 45,
"emoji": "✂",
"description": "Scissors",
"unicode": "U+2702U+FE0F"
}, {
"number": 46,
"emoji": "🔒",
"description": "Lock",
"unicode": "U+1F512"
}, {
"number": 47,
"emoji": "🔑",
"description": "Key",
"unicode": "U+1F511"
}, {
"number": 48,
"emoji": "🔨",
"description": "Hammer",
"unicode": "U+1F528"
}, {
"number": 49,
"emoji": "☎",
"description": "Telephone",
"unicode": "U+260EU+FE0F"
}, {
"number": 50,
"emoji": "🏁",
"description": "Flag",
"unicode": "U+1F3C1"
}, {
"number": 51,
"emoji": "🚂",
"description": "Train",
"unicode": "U+1F682"
}, {
"number": 52,
"emoji": "🚲",
"description": "Bicycle",
"unicode": "U+1F6B2"
}, {
"number": 53,
"emoji": "✈",
"description": "Aeroplane",
"unicode": "U+2708U+FE0F"
}, {
"number": 54,
"emoji": "🚀",
"description": "Rocket",
"unicode": "U+1F680"
}, {
"number": 55,
"emoji": "🏆",
"description": "Trophy",
"unicode": "U+1F3C6"
}, {
"number": 56,
"emoji": "⚽",
"description": "Ball",
"unicode": "U+26BD"
}, {
"number": 57,
"emoji": "🎸",
"description": "Guitar",
"unicode": "U+1F3B8"
}, {
"number": 58,
"emoji": "🎺",
"description": "Trumpet",
"unicode": "U+1F3BA"
}, {
"number": 59,
"emoji": "🔔",
"description": "Bell",
"unicode": "U+1F514"
}, {
"number": 60,
"emoji": "⚓",
"description": "Anchor",
"unicode": "U+2693"
}, {
"number": 61,
"emoji": "🎧",
"description": "Headphones",
"unicode": "U+1F3A7"
}, {
"number": 62,
"emoji": "📁",
"description": "Folder",
"unicode": "U+1F4C1"
}, {
"number": 63,
"emoji": "📌",
"description": "Pin",
"unicode": "U+1F4CC"
}]
Layout.alignment: Qt.AlignHCenter
Repeater {
id: repeater
model: 7
delegate: Rectangle {
color: "transparent"
implicitHeight: Qt.application.font.pixelSize * 8
implicitWidth: col.width
ColumnLayout {
id: col
property var emoji: emojis.mapping[flow.sasList[index]]
Layout.fillWidth: true
anchors.bottom: parent.bottom
Label {
//height: font.pixelSize * 2
Layout.alignment: Qt.AlignHCenter
text: col.emoji.emoji
font.pixelSize: Qt.application.font.pixelSize * 2
font.family: Settings.emojiFont
color: colors.text
}
Label {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
text: col.emoji.description
color: colors.text
}
}
}
}
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("They do not match!")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("They match!")
onClicked: flow.next()
}
}
}
}

@ -0,0 +1,56 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Verification failed")
ColumnLayout {
spacing: 16
Text {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: {
switch (flow.error) {
case DeviceVerificationFlow.UnknownMethod:
return qsTr("Other client does not support our verification protocol.");
case DeviceVerificationFlow.MismatchedCommitment:
case DeviceVerificationFlow.MismatchedSAS:
case DeviceVerificationFlow.KeyMismatch:
return qsTr("Key mismatch detected!");
case DeviceVerificationFlow.Timeout:
return qsTr("Device verification timed out.");
case DeviceVerificationFlow.User:
return qsTr("Other party canceled the verification.");
case DeviceVerificationFlow.OutOfOrder:
return qsTr("Device verification timed out.");
default:
return "Unknown verification error.";
}
}
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("Close")
onClicked: dialog.close()
}
}
}
}

@ -0,0 +1,46 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
ColumnLayout {
spacing: 16
Label {
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: flow.sender ? qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") : qsTr("The device was requested to be verified")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
onClicked: flow.next()
}
}
}
}

@ -0,0 +1,38 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
Pane {
property string title: qsTr("Successful Verification")
ColumnLayout {
spacing: 16
Label {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Verification successful! Both sides verified their devices!")
color: colors.text
verticalAlignment: Text.AlignVCenter
}
RowLayout {
Item {
Layout.fillWidth: true
}
Button {
Layout.alignment: Qt.AlignRight
text: qsTr("Close")
onClicked: dialog.close()
}
}
}
}

@ -0,0 +1,56 @@
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane {
property string title: qsTr("Waiting for other party")
ColumnLayout {
spacing: 16
Label {
id: content
Layout.maximumWidth: 400
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.Wrap
text: {
switch (flow.state) {
case "WaitingForOtherToAccept":
return qsTr("Waiting for other side to accept the verification request.");
case "WaitingForKeys":
return qsTr("Waiting for other side to continue the verification request.");
case "WaitingForMac":
return qsTr("Waiting for other side to complete the verification request.");
}
}
color: colors.text
verticalAlignment: Text.AlignVCenter
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
palette: colors
}
RowLayout {
Button {
Layout.alignment: Qt.AlignLeft
text: qsTr("Cancel")
onClicked: {
flow.cancel();
dialog.close();
}
}
Item {
Layout.fillWidth: true
}
}
}
}

@ -0,0 +1,66 @@
[
{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
{"number": 21, "emoji": "☁", "description": "Cloud", "unicode": "U+2601U+FE0F"},
{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
{"number": 29, "emoji": "❤", "description": "Heart", "unicode": "U+2764U+FE0F"},
{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
{"number": 37, "emoji": "☂", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
{"number": 43, "emoji": "✏", "description": "Pencil", "unicode": "U+270FU+FE0F"},
{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
{"number": 45, "emoji": "✂", "description": "Scissors", "unicode": "U+2702U+FE0F"},
{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
{"number": 49, "emoji": "☎", "description": "Telephone", "unicode": "U+260EU+FE0F"},
{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
{"number": 53, "emoji": "✈", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
]

@ -0,0 +1,16 @@
import "../"
import QtQuick 2.10
import QtQuick.Controls 2.1
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
ImageButton {
id: emojiButton
property var colors: currentActivePalette
property var emojiPicker
property string event_id
image: ":/icons/icons/ui/smile.png"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id)
}

@ -0,0 +1,332 @@
import "../"
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
Popup {
id: emojiPopup
property string event_id
property var colors
property alias model: gridView.model
property var textArea
property string emojiCategory: "people"
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
function show(showAt, event_id) {
console.debug("Showing emojiPicker for " + event_id);
if (showAt) {
parent = showAt;
x = Math.round((showAt.width - width) / 2);
y = showAt.height;
}
emojiPopup.event_id = event_id;
open();
}
margins: 0
bottomPadding: 1
leftPadding: 1
rightPadding: 1
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
ColumnLayout {
id: columnView
anchors.fill: parent
spacing: 0
Layout.bottomMargin: 0
Layout.leftMargin: 3
Layout.rightMargin: 3
Layout.topMargin: 2
// emoji grid
GridView {
id: gridView
Layout.preferredHeight: emojiPopup.height
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 4
cellWidth: 52
cellHeight: 52
boundsBehavior: Flickable.StopAtBounds
clip: true
currentIndex: -1 // prevent sorting from stealing focus
// Individual emoji
delegate: AbstractButton {
width: 48
height: 48
hoverEnabled: true
ToolTip.text: model.shortName
ToolTip.visible: hovered
// TODO: maybe add favorites at some point?
onClicked: {
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id);
emojiPopup.close();
TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode);
}
// give the emoji a little oomf
DropShadow {
width: parent.width
height: parent.height
horizontalOffset: 3
verticalOffset: 3
radius: 8
samples: 17
color: "#80000000"
source: parent.contentItem
}
contentItem: Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: Settings.emojiFont
font.pixelSize: 36
text: model.unicode
}
background: Rectangle {
anchors.fill: parent
color: hovered ? colors.highlight : 'transparent'
radius: 5
}
}
// Search field
header: TextField {
id: emojiSearch
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: emojiScroll.width + 4
placeholderText: qsTr("Search")
selectByMouse: true
rightPadding: clearSearch.width
onTextChanged: searchTimer.restart()
onVisibleChanged: {
if (visible)
forceActiveFocus();
}
Timer {
id: searchTimer
interval: 350 // tweak as needed?
onTriggered: {
emojiPopup.model.filter = emojiSearch.text;
emojiPopup.model.category = EmojiCategory.Search;
}
}
ToolButton {
id: clearSearch
visible: emojiSearch.text !== ''
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
focusPolicy: Qt.NoFocus
onClicked: emojiSearch.clear()
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
// clear the default hover effects.
background: Item {
}
}
}
ScrollBar.vertical: ScrollBar {
id: emojiScroll
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: emojiPopup.colors.dark
}
// Category picker row
RowLayout {
Layout.bottomMargin: 0
Layout.preferredHeight: 42
implicitHeight: 42
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
// Display the normal categories
Repeater {
model: ListModel {
// TODO: Would like to get 'simple' icons for the categories
ListElement {
image: ":/icons/icons/emoji-categories/people.png"
category: EmojiCategory.People
}
ListElement {
image: ":/icons/icons/emoji-categories/nature.png"
category: EmojiCategory.Nature
}
ListElement {
image: ":/icons/icons/emoji-categories/foods.png"
category: EmojiCategory.Food
}
ListElement {
image: ":/icons/icons/emoji-categories/activity.png"
category: EmojiCategory.Activity
}
ListElement {
image: ":/icons/icons/emoji-categories/travel.png"
category: EmojiCategory.Travel
}
ListElement {
image: ":/icons/icons/emoji-categories/objects.png"
category: EmojiCategory.Objects
}
ListElement {
image: ":/icons/icons/emoji-categories/symbols.png"
category: EmojiCategory.Symbols
}
ListElement {
image: ":/icons/icons/emoji-categories/flags.png"
category: EmojiCategory.Flags
}
}
delegate: AbstractButton {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
hoverEnabled: true
ToolTip.text: {
switch (model.category) {
case EmojiCategory.People:
return qsTr('People');
case EmojiCategory.Nature:
return qsTr('Nature');
case EmojiCategory.Food:
return qsTr('Food');
case EmojiCategory.Activity:
return qsTr('Activity');
case EmojiCategory.Travel:
return qsTr('Travel');
case EmojiCategory.Objects:
return qsTr('Objects');
case EmojiCategory.Symbols:
return qsTr('Symbols');
case EmojiCategory.Flags:
return qsTr('Flags');
}
}
ToolTip.visible: hovered
onClicked: {
emojiPopup.model.category = model.category;
}
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}
contentItem: Image {
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
fillMode: Image.Pad
sourceSize.width: 32
sourceSize.height: 32
source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText)
}
background: Rectangle {
anchors.fill: parent
color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent'
radius: 5
border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent'
}
}
}
// Separator
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 1
implicitWidth: 1
height: parent.height
color: emojiPopup.colors.dark
}
// Search Button is special
AbstractButton {
id: searchBtn
hoverEnabled: true
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: 0
ToolTip.text: qsTr("Search")
ToolTip.visible: hovered
onClicked: {
// clear any filters
emojiPopup.model.category = EmojiCategory.Search;
gridView.positionViewAtBeginning();
emojiSearch.forceActiveFocus();
}
Layout.preferredWidth: 36
Layout.preferredHeight: 36
implicitWidth: 36
implicitHeight: 36
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: mouse.accepted = false
cursorShape: Qt.PointingHandCursor
}
contentItem: Image {
anchors.right: parent.right
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
sourceSize.width: 32
sourceSize.height: 32
fillMode: Image.Pad
smooth: true
source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText)
}
}
}
}
}

@ -0,0 +1,2 @@
[Controls]
FallbackStyle=Fusion

@ -14,12 +14,16 @@
<file>icons/ui/double-tick-indicator@2x.png</file>
<file>icons/ui/lock.png</file>
<file>icons/ui/lock@2x.png</file>
<file>icons/ui/unlock.png</file>
<file>icons/ui/unlock@2x.png</file>
<file>icons/ui/clock.png</file>
<file>icons/ui/clock@2x.png</file>
<file>icons/ui/checkmark.png</file>
<file>icons/ui/checkmark@2x.png</file>
<file>icons/ui/cursor.png</file>
<file>icons/ui/cursor@2x.png</file>
<file>icons/ui/search.png</file>
<file>icons/ui/search@2x.png</file>
<file>icons/ui/settings.png</file>
<file>icons/ui/settings@2x.png</file>
<file>icons/ui/smile.png</file>
@ -66,6 +70,11 @@
<file>icons/ui/mail-reply.png</file>
<file>icons/ui/place-call.png</file>
<file>icons/ui/end-call.png</file>
<file>icons/ui/microphone-mute.png</file>
<file>icons/ui/microphone-unmute.png</file>
<file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/people@2x.png</file>
<file>icons/emoji-categories/nature.png</file>
@ -109,13 +118,21 @@
<file>styles/nheko-dark.qss</file>
</qresource>
<qresource prefix="/">
<file>qtquickcontrols2.conf</file>
<file>qml/TimelineView.qml</file>
<file>qml/ActiveCallBar.qml</file>
<file>qml/Avatar.qml</file>
<file>qml/ImageButton.qml</file>
<file>qml/MatrixText.qml</file>
<file>qml/StatusIndicator.qml</file>
<file>qml/EncryptionIndicator.qml</file>
<file>qml/Reactions.qml</file>
<file>qml/ScrollHelper.qml</file>
<file>qml/TimelineRow.qml</file>
<file>qml/emoji/EmojiButton.qml</file>
<file>qml/emoji/EmojiPicker.qml</file>
<file>qml/UserProfile.qml</file>
<file>qml/delegates/MessageDelegate.qml</file>
<file>qml/delegates/TextMessage.qml</file>
<file>qml/delegates/NoticeMessage.qml</file>
@ -125,5 +142,17 @@
<file>qml/delegates/Pill.qml</file>
<file>qml/delegates/Placeholder.qml</file>
<file>qml/delegates/Reply.qml</file>
<file>qml/device-verification/Waiting.qml</file>
<file>qml/device-verification/DeviceVerification.qml</file>
<file>qml/device-verification/DigitVerification.qml</file>
<file>qml/device-verification/EmojiVerification.qml</file>
<file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Failed.qml</file>
<file>qml/device-verification/Success.qml</file>
</qresource>
<qresource prefix="/media">
<file>media/ring.ogg</file>
<file>media/ringback.ogg</file>
<file>media/callend.ogg</file>
</qresource>
</RCC>

@ -31,7 +31,6 @@ UserMentionsWidget > * {
}
QLineEdit,
QListWidget,
WelcomePage,
LoginPage,
RegisterPage,
@ -160,7 +159,7 @@ CommunitiesListItem {
qproperty-backgroundColor: #2d3139;
qproperty-avatarBgColor: #202228;
qproperty-avatarFgColor: white;
qproperty-avatarFgColor: palette(window);
}
LoadingIndicator {
@ -177,14 +176,6 @@ UserInfoWidget {
border-bottom: 1px solid #202228;
}
#UserSettingScrollWidget > QComboBox {
color: #202228;
}
#UserSettingScrollWidget > QComboBox {
color: #202228;
}
Avatar {
qproperty-textColor: white;
qproperty-backgroundColor: #2d3139;
@ -214,11 +205,6 @@ TextField {
qproperty-labelColor: #caccd1;
}
ScrollBar {
qproperty-handleColor: #2d3139;
qproperty-backgroundColor: #202228;
}
SideBarActions,
TopRoomBar
{
@ -246,6 +232,11 @@ Toggle {
qproperty-trackColor: rgb(240, 240, 240);
}
QListWidget {
color: #caccd1;
background-color: #202228;
}
SnackBar {
qproperty-textColor: #caccd1;
qproperty-bgColor: #202228;

@ -183,6 +183,8 @@ TopSection {
WelcomePage,
LoginPage,
QComboBox,
QPushButton,
RegisterPage {
background-color: white;
color: #333;
@ -221,16 +223,19 @@ TextField {
qproperty-labelColor: #333;
}
QListWidget,
TextInputWidget,
QTextEdit,
QLineEdit {
background-color: white;
color: #333;
}
TextInputWidget {
border: none;
border-top: 1px solid #dcdcdc;
}
ScrollBar {
qproperty-handleColor: #ccc;
qproperty-backgroundColor: #efefef;
}
SideBarActions {
border: none;
border-top: 1px solid #dcdcdc;

@ -70,7 +70,7 @@ FileItem {
}
RaisedButton {
qproperty-foregroundColor: palette(buttonText);
qproperty-foregroundColor: palette(button-text);
}
TextField {
@ -95,18 +95,18 @@ UserMentionsWidget {
qproperty-titleColor: palette(text);
qproperty-subtitleColor: palette(text);
qproperty-highlightedTitleColor: palette(highlightedtext);
qproperty-highlightedSubtitleColor: palette(highlightedtext);
qproperty-highlightedTitleColor: palette(highlighted-text);
qproperty-highlightedSubtitleColor: palette(highlighted-text);
qproperty-hoverTitleColor: palette(highlightedtext);
qproperty-hoverSubtitleColor: palette(highlightedtext);
qproperty-hoverTitleColor: palette(dark);
qproperty-hoverSubtitleColor: palette(dark);
qproperty-btnColor: palette(button);
qproperty-btnTextColor: palette(buttonText);
qproperty-btnColor: palette(dark);
qproperty-btnTextColor: palette(bright-text);
qproperty-timestampColor: palette(text);
qproperty-highlightedTimestampColor: palette(highlightedtext);
qproperty-hoverTimestampColor: palette(highlightedtext);
qproperty-highlightedTimestampColor: palette(highlighted-text);
qproperty-hoverTimestampColor: palette(dark);
qproperty-bubbleBgColor: palette(base);
qproperty-bubbleFgColor: palette(text);

@ -14,8 +14,9 @@ class Emoji(object):
def generate_code(emojis, category):
tmpl = Template('''
const std::vector<Emoji> emoji::Provider::{{ category }} = {
// {{ category.capitalize() }}
{%- for e in emoji %}
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}"},
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ category.capitalize() }}},
{%- endfor %}
};
''')
@ -23,6 +24,19 @@ const std::vector<Emoji> emoji::Provider::{{ category }} = {
d = dict(category=category, emoji=emojis)
print(tmpl.render(d))
def generate_qml_list(**kwargs):
tmpl = Template('''
const QVector<Emoji> emoji::Provider::emoji = {
{%- for c in kwargs.items() %}
// {{ c[0].capitalize() }}
{%- for e in c[1] %}
Emoji{QString::fromUtf8("{{ e.code }}"), "{{ e.shortname }}", emoji::EmojiCategory::{{ c[0].capitalize() }}},
{%- endfor %}
{%- endfor %}
};
''')
d = dict(kwargs=kwargs)
print(tmpl.render(d))
if __name__ == '__main__':
if len(sys.argv) < 2:
@ -87,3 +101,4 @@ if __name__ == '__main__':
generate_code(objects, 'objects')
generate_code(symbols, 'symbols')
generate_code(flags, 'flags')
generate_qml_list(people=people, nature=nature, food=food, activity=activity, travel=travel, objects=objects, symbols=symbols, flags=flags)

@ -9,19 +9,35 @@ set -eu
INPUT=$1
OUTPUT=nheko
filename=$(basename -- "$1")
extension="${filename##*.}"
mkdir ${OUTPUT}.iconset
sips -z 16 16 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16.png
sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16@2x.png
sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32.png
sips -z 64 64 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32@2x.png
sips -z 128 128 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128.png
sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128@2x.png
sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256.png
sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256@2x.png
sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_512x512.png
cp "${INPUT}" ${OUTPUT}.iconset/icon_512x512@2x.png
if [ extension = "svg" ]; then
rsvg-convert -h 16 "${INPUT}" > ${OUTPUT}.iconset/icon_16x16.png
rsvg-convert -h 32 "${INPUT}" > ${OUTPUT}.iconset/icon_16x16@2x.png
rsvg-convert -h 32 "${INPUT}" > ${OUTPUT}.iconset/icon_32x32.png
rsvg-convert -h 64 "${INPUT}" > ${OUTPUT}.iconset/icon_32x32@2x.png
rsvg-convert -h 128 "${INPUT}" > ${OUTPUT}.iconset/icon_128x128.png
rsvg-convert -h 256 "${INPUT}" > ${OUTPUT}.iconset/icon_128x128@2x.png
rsvg-convert -h 256 "${INPUT}" > ${OUTPUT}.iconset/icon_256x256.png
rsvg-convert -h 512 "${INPUT}" > ${OUTPUT}.iconset/icon_256x256@2x.png
rsvg-convert -h 512 "${INPUT}" > ${OUTPUT}.iconset/icon_512x512.png
rsvg-convert -h 1024 "${INPUT}" > ${OUTPUT}.iconset/icon_512x512@2x.png
else
sips -z 16 16 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16.png
sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_16x16@2x.png
sips -z 32 32 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32.png
sips -z 64 64 "${INPUT}" --out ${OUTPUT}.iconset/icon_32x32@2x.png
sips -z 128 128 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128.png
sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_128x128@2x.png
sips -z 256 256 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256.png
sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_256x256@2x.png
sips -z 512 512 "${INPUT}" --out ${OUTPUT}.iconset/icon_512x512.png
cp "${INPUT}" ${OUTPUT}.iconset/icon_512x512@2x.png
fi
iconutil -c icns ${OUTPUT}.iconset

@ -24,6 +24,7 @@
#include "Cache.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
static QPixmapCache avatar_cache;
@ -33,10 +34,12 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
{
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
if (avatarUrl.isEmpty())
QPixmap pixmap;
if (avatarUrl.isEmpty()) {
callback(pixmap);
return;
}
QPixmap pixmap;
if (avatar_cache.find(cacheKey, &pixmap)) {
callback(pixmap);
return;
@ -44,7 +47,7 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
auto data = cache::image(cacheKey);
if (!data.isNull()) {
pixmap.loadFromData(data);
pixmap = QPixmap::fromImage(utils::readImage(&data));
avatar_cache.insert(cacheKey, pixmap);
callback(pixmap);
return;
@ -54,9 +57,8 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
QObject::connect(proxy.get(),
&AvatarProxy::avatarDownloaded,
receiver,
[callback, cacheKey](const QByteArray &data) {
QPixmap pm;
pm.loadFromData(data);
[callback, cacheKey](QByteArray data) {
QPixmap pm = QPixmap::fromImage(utils::readImage(&data));
avatar_cache.insert(cacheKey, pm);
callback(pm);
});
@ -75,10 +77,9 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
}
} else {
cache::saveImage(cacheKey.toStdString(), res);
}
emit proxy->avatarDownloaded(QByteArray(res.data(), res.size()));
});

@ -0,0 +1,38 @@
#include "BlurhashProvider.h"
#include <algorithm>
#include <QUrl>
#include "blurhash.hpp"
void
BlurhashResponse::run()
{
if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) {
m_error = QStringLiteral("Blurhash needs size request");
emit finished();
return;
}
if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) {
m_image = QImage(m_requestedSize, QImage::Format_RGB32);
m_image.fill(QColor(0, 0, 0));
emit finished();
return;
}
auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(),
m_requestedSize.width(),
m_requestedSize.height(),
4);
if (decoded.image.empty()) {
m_error = QStringLiteral("Failed decode!");
emit finished();
return;
}
QImage image(decoded.image.data(), decoded.width, decoded.height, QImage::Format_RGB32);
m_image = image.copy();
emit finished();
}

@ -0,0 +1,51 @@
#pragma once
#include <QQuickAsyncImageProvider>
#include <QQuickImageResponse>
#include <QImage>
#include <QThreadPool>
class BlurhashResponse
: public QQuickImageResponse
, public QRunnable
{
public:
BlurhashResponse(const QString &id, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
{
setAutoDelete(false);
}
QQuickTextureFactory *textureFactory() const override
{
return QQuickTextureFactory::textureFactoryForImage(m_image);
}
QString errorString() const override { return m_error; }
void run() override;
QString m_id, m_error;
QSize m_requestedSize;
QImage m_image;
};
class BlurhashProvider
: public QObject
, public QQuickAsyncImageProvider
{
Q_OBJECT
public slots:
QQuickImageResponse *requestImageResponse(const QString &id,
const QSize &requestedSize) override
{
BlurhashResponse *response = new BlurhashResponse(id, requestedSize);
pool.start(response);
return response;
}
private:
QThreadPool pool;
};

File diff suppressed because it is too large Load Diff

@ -54,6 +54,26 @@ insertDisplayName(const QString &room_id, const QString &user_id, const QString
void
insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url);
// presence
mtx::presence::PresenceState
presenceState(const std::string &user_id);
std::string
statusMessage(const std::string &user_id);
// user cache stores user keys
std::optional<UserKeyCache>
userKeys(const std::string &user_id);
void
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
// device & user verification cache
std::optional<VerificationStatus>
verificationStatus(const std::string &user_id);
void
markDeviceVerified(const std::string &user_id, const std::string &device);
void
markDeviceUnverified(const std::string &user_id, const std::string &device);
//! Load saved data for the display names & avatars.
void
populateMembers();
@ -111,10 +131,15 @@ removeRoom(const QString &roomid);
void
setup();
bool
isFormatValid();
//! returns if the format is current, older or newer
cache::CacheVersion
formatVersion();
//! set the format version to the current version
void
setCurrentFormat();
//! migrates db to the current format
bool
runMigrations();
std::map<QString, mtx::responses::Timeline>
roomMessages();
@ -154,21 +179,6 @@ using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>
UserReceipts
readReceipts(const QString &event_id, const QString &room_id);
//! Filter the events that have at least one read receipt.
std::vector<QString>
filterReadEvents(const QString &room_id,
const std::vector<QString> &event_ids,
const std::string &excluded_user);
//! Add event for which we are expecting some read receipts.
void
addPendingReceipt(const QString &room_id, const QString &event_id);
void
removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id);
void
notifyForReadReceipts(const std::string &room_id);
std::vector<QString>
pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id);
QByteArray
image(const QString &url);
QByteArray
@ -259,6 +269,8 @@ bool
outboundMegolmSessionExists(const std::string &room_id) noexcept;
void
updateOutboundMegolmSession(const std::string &room_id, int message_index);
void
dropOutboundMegolmSession(const std::string &room_id);
void
importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);

@ -65,3 +65,53 @@ struct OlmSessionStorage
std::mutex group_outbound_mtx;
std::mutex group_inbound_mtx;
};
//! Verification status of a single user
struct VerificationStatus
{
//! True, if the users master key is verified
bool user_verified = false;
//! List of all devices marked as verified
std::vector<std::string> verified_devices;
};
//! In memory cache of verification status
struct VerificationStorage
{
//! mapping of user to verification status
std::map<std::string, VerificationStatus> status;
std::mutex verification_storage_mtx;
};
// this will store the keys of the user with whom a encrypted room is shared with
struct UserKeyCache
{
//! Device id to device keys
std::map<std::string, mtx::crypto::DeviceKeys> device_keys;
//! corss signing keys
mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
//! Sync token when nheko last fetched the keys
std::string updated_at;
//! Sync token when the keys last changed. updated != last_changed means they are outdated.
std::string last_changed;
};
void
to_json(nlohmann::json &j, const UserKeyCache &info);
void
from_json(const nlohmann::json &j, UserKeyCache &info);
// the reason these are stored in a seperate cache rather than storing it in the user cache is
// UserKeyCache stores only keys of users with which encrypted room is shared
struct VerificationCache
{
//! list of verified device_ids with device-verification
std::vector<std::string> device_verified;
//! list of devices the user blocks
std::vector<std::string> device_blocked;
};
void
to_json(nlohmann::json &j, const VerificationCache &info);
void
from_json(const nlohmann::json &j, VerificationCache &info);

@ -8,6 +8,15 @@
#include <mtx/events/join_rules.hpp>
namespace cache {
enum class CacheVersion : int
{
Older = -1,
Current = 0,
Newer = 1,
};
}
struct RoomMember
{
QString user_id;
@ -39,7 +48,8 @@ struct DescInfo
QString event_id;
QString userid;
QString body;
QString timestamp;
QString descriptiveTime;
uint64_t timestamp;
QDateTime datetime;
};

@ -18,6 +18,7 @@
#pragma once
#include <limits>
#include <optional>
#include <QDateTime>
@ -38,9 +39,6 @@
#include "CacheCryptoStructs.h"
#include "CacheStructs.h"
int
numeric_key_comparison(const MDB_val *a, const MDB_val *b);
class Cache : public QObject
{
Q_OBJECT
@ -52,6 +50,27 @@ public:
static QString displayName(const QString &room_id, const QString &user_id);
static QString avatarUrl(const QString &room_id, const QString &user_id);
// presence
mtx::presence::PresenceState presenceState(const std::string &user_id);
std::string statusMessage(const std::string &user_id);
// user cache stores user keys
std::optional<UserKeyCache> userKeys(const std::string &user_id);
void updateUserKeys(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery);
void markUserKeysOutOfDate(lmdb::txn &txn,
lmdb::dbi &db,
const std::vector<std::string> &user_ids,
const std::string &sync_token);
void deleteUserKeys(lmdb::txn &txn,
lmdb::dbi &db,
const std::vector<std::string> &user_ids);
// device & user verification cache
VerificationStatus verificationStatus(const std::string &user_id);
void markDeviceVerified(const std::string &user_id, const std::string &device);
void markDeviceUnverified(const std::string &user_id, const std::string &device);
static void removeDisplayName(const QString &room_id, const QString &user_id);
static void removeAvatarUrl(const QString &room_id, const QString &user_id);
@ -102,8 +121,9 @@ public:
void removeRoom(const std::string &roomid);
void setup();
bool isFormatValid();
cache::CacheVersion formatVersion();
void setCurrentFormat();
bool runMigrations();
std::map<QString, mtx::responses::Timeline> roomMessages();
@ -137,18 +157,6 @@ public:
using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
UserReceipts readReceipts(const QString &event_id, const QString &room_id);
//! Filter the events that have at least one read receipt.
std::vector<QString> filterReadEvents(const QString &room_id,
const std::vector<QString> &event_ids,
const std::string &excluded_user);
//! Add event for which we are expecting some read receipts.
void addPendingReceipt(const QString &room_id, const QString &event_id);
void removePendingReceipt(lmdb::txn &txn,
const std::string &room_id,
const std::string &event_id);
void notifyForReadReceipts(const std::string &room_id);
std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id);
QByteArray image(const QString &url) const;
QByteArray image(lmdb::txn &txn, const std::string &url) const;
void saveImage(const std::string &url, const std::string &data);
@ -179,6 +187,47 @@ public:
//! Add all notifications containing a user mention to the db.
void saveTimelineMentions(const mtx::responses::Notifications &res);
//! retrieve events in timeline and related functions
struct Messages
{
mtx::responses::Timeline timeline;
uint64_t next_index;
bool end_of_cache = false;
};
Messages getTimelineMessages(lmdb::txn &txn,
const std::string &room_id,
uint64_t index = std::numeric_limits<uint64_t>::max(),
bool forward = false);
std::optional<mtx::events::collections::TimelineEvent> getEvent(
const std::string &room_id,
const std::string &event_id);
void storeEvent(const std::string &room_id,
const std::string &event_id,
const mtx::events::collections::TimelineEvent &event);
std::vector<std::string> relatedEvents(const std::string &room_id,
const std::string &event_id);
struct TimelineRange
{
uint64_t first, last;
};
std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
std::optional<uint64_t> getTimelineIndex(const std::string &room_id,
std::string_view event_id);
std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
std::string previousBatchToken(const std::string &room_id);
uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
void savePendingMessage(const std::string &room_id,
const mtx::events::collections::TimelineEvent &message);
std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage(
const std::string &room_id);
void removePendingStatus(const std::string &room_id, const std::string &txn_id);
//! clear timeline keeping only the latest batch
void clearTimeline(const std::string &room_id);
//! Remove old unused data.
void deleteOldMessages();
void deleteOldData() noexcept;
@ -201,6 +250,7 @@ public:
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
void updateOutboundMegolmSession(const std::string &room_id, int message_index);
void dropOutboundMegolmSession(const std::string &room_id);
void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
mtx::crypto::ExportedSessionKeys exportSessionKeys();
@ -229,6 +279,10 @@ public:
signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void roomReadStatus(const std::map<QString, bool> &status);
void removeNotification(const QString &room_id, const QString &event_id);
void userKeysUpdate(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery);
void verificationStatusChanged(const std::string &userid);
private:
//! Save an invited room.
@ -256,7 +310,13 @@ private:
const std::string &room_id,
const mtx::responses::Timeline &res);
mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id);
//! retrieve a specific event from account data
//! pass empty room_id for global account data
std::optional<mtx::events::collections::RoomAccountDataEvents>
getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
bool isHiddenEvent(lmdb::txn &txn,
mtx::events::collections::TimelineEvents e,
const std::string &room_id);
//! Remove a room from the cache.
// void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
@ -387,6 +447,10 @@ private:
void saveInvites(lmdb::txn &txn,
const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
void savePresence(
lmdb::txn &txn,
const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates);
//! Sends signals for the rooms that are removed.
void removeLeftRooms(lmdb::txn &txn,
const std::map<std::string, mtx::responses::LeftRoom> &rooms)
@ -404,13 +468,46 @@ private:
return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
}
lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id)
lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
}
lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
{
auto db =
lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE);
lmdb::dbi_set_compare(txn, db, numeric_key_comparison);
return lmdb::dbi::open(
txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
return db;
// inverse of EventOrderDb
lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
}
lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
}
lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
}
lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
@ -430,6 +527,12 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
}
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
}
lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
@ -440,6 +543,21 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE);
}
lmdb::dbi getPresenceDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "presence", MDB_CREATE);
}
lmdb::dbi getUserKeysDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
}
lmdb::dbi getVerificationDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "verified", MDB_CREATE);
}
//! Retrieves or creates the database that stores the open OLM sessions between our device
//! and the given curve25519 key which represents another device.
//!
@ -458,6 +576,8 @@ private:
return QString::fromStdString(event.state_key);
}
std::optional<VerificationCache> verificationCache(const std::string &user_id);
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
void setNextBatchToken(lmdb::txn &txn, const QString &token);
@ -482,6 +602,7 @@ private:
static QHash<QString, QString> AvatarUrls;
OlmSessionStorage session_storage;
VerificationStorage verification_storage;
};
namespace cache {

@ -0,0 +1,457 @@
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <QMediaPlaylist>
#include <QUrl>
#include "Cache.h"
#include "CallManager.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "UserSettingsPage.h"
#include "WebRTCSession.h"
#include "dialogs/AcceptCall.h"
#include "mtx/responses/turn_server.hpp"
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
Q_DECLARE_METATYPE(mtx::responses::TurnServer)
using namespace mtx::events;
using namespace mtx::events::msg;
// https://github.com/vector-im/riot-web/issues/10173
#define STUN_SERVER "stun://turn.matrix.org:3478"
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer);
}
CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
: QObject()
, session_(WebRTCSession::instance())
, turnServerTimer_(this)
, settings_(userSettings)
{
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
qRegisterMetaType<mtx::responses::TurnServer>();
connect(
&session_,
&WebRTCSession::offerCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_});
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
QTimer::singleShot(timeoutms_, this, [this]() {
if (session_.state() == webrtc::State::OFFERSENT) {
hangUp(CallHangUp::Reason::InviteTimeOut);
emit ChatPage::instance()->showNotification(
"The remote side failed to pick up.");
}
});
});
connect(
&session_,
&WebRTCSession::answerCreated,
this,
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
emit newMessage(roomid_, CallAnswer{callid_, sdp, 0});
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
});
connect(&session_,
&WebRTCSession::newICECandidate,
this,
[this](const CallCandidates::Candidate &candidate) {
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0});
});
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
connect(this,
&CallManager::turnServerRetrieved,
this,
[this](const mtx::responses::TurnServer &res) {
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
nhlog::net()->info("username: {}", res.username);
nhlog::net()->info("ttl: {} seconds", res.ttl);
for (const auto &u : res.uris)
nhlog::net()->info("uri: {}", u);
// Request new credentials close to expiry
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
turnURIs_ = getTurnURIs(res);
uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
if (res.ttl < 3600)
nhlog::net()->warn("Setting ttl to 1 hour");
turnServerTimer_.setInterval(ttl * 1000 * 0.9);
});
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
switch (state) {
case webrtc::State::DISCONNECTED:
playRingtone("qrc:/media/media/callend.ogg", false);
clear();
break;
case webrtc::State::ICEFAILED: {
QString error("Call connection failed.");
if (turnURIs_.empty())
error += " Your homeserver has no configured TURN server.";
emit ChatPage::instance()->showNotification(error);
hangUp(CallHangUp::Reason::ICEFailed);
break;
}
default:
break;
}
});
connect(&player_,
&QMediaPlayer::mediaStatusChanged,
this,
[this](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::LoadedMedia)
player_.play();
});
}
void
CallManager::sendInvite(const QString &roomid)
{
if (onActiveCall())
return;
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
if (roomInfo.member_count != 2) {
emit ChatPage::instance()->showNotification(
"Voice calls are limited to 1:1 rooms.");
return;
}
std::string errorMessage;
if (!session_.init(&errorMessage)) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
return;
}
roomid_ = roomid;
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
session_.setTurnServers(turnURIs_);
generateCallID();
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
const RoomMember &callee =
members.front().user_id == utils::localUser() ? members.back() : members.front();
callPartyName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
emit newCallParty();
playRingtone("qrc:/media/media/ringback.ogg", true);
if (!session_.createOffer()) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall();
}
}
namespace {
std::string
callHangUpReasonString(CallHangUp::Reason reason)
{
switch (reason) {
case CallHangUp::Reason::ICEFailed:
return "ICE failed";
case CallHangUp::Reason::InviteTimeOut:
return "Invite time out";
default:
return "User";
}
}
}
void
CallManager::hangUp(CallHangUp::Reason reason)
{
if (!callid_.empty()) {
nhlog::ui()->debug(
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
endCall();
}
}
bool
CallManager::onActiveCall() const
{
return session_.state() != webrtc::State::DISCONNECTED;
}
void
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
{
#ifdef GSTREAMER_AVAILABLE
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) ||
handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event))
return;
#else
(void)event;
#endif
}
template<typename T>
bool
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event)
{
if (std::holds_alternative<RoomEvent<T>>(event)) {
handleEvent(std::get<RoomEvent<T>>(event));
return true;
}
return false;
}
void
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
{
const char video[] = "m=video";
const std::string &sdp = callInviteEvent.content.sdp;
bool isVideo = std::search(sdp.cbegin(),
sdp.cend(),
std::cbegin(video),
std::cend(video) - 1,
[](unsigned char c1, unsigned char c2) {
return std::tolower(c1) == std::tolower(c2);
}) != sdp.cend();
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
callInviteEvent.content.call_id,
(isVideo ? "video" : "voice"),
callInviteEvent.sender);
if (callInviteEvent.content.call_id.empty())
return;
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
if (onActiveCall() || roomInfo.member_count != 2 || isVideo) {
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
0,
CallHangUp::Reason::InviteTimeOut});
return;
}
playRingtone("qrc:/media/media/ring.ogg", true);
roomid_ = QString::fromStdString(callInviteEvent.room_id);
callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear();
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
const RoomMember &caller =
members.front().user_id == utils::localUser() ? members.back() : members.front();
callPartyName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
emit newCallParty();
auto dialog = new dialogs::AcceptCall(caller.user_id,
caller.display_name,
QString::fromStdString(roomInfo.name),
QString::fromStdString(roomInfo.avatar_url),
settings_,
MainWindow::instance());
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() {
MainWindow::instance()->hideOverlay();
answerInvite(callInviteEvent.content);
});
connect(dialog, &dialogs::AcceptCall::reject, this, [this]() {
MainWindow::instance()->hideOverlay();
hangUp();
});
MainWindow::instance()->showSolidOverlayModal(dialog);
}
void
CallManager::answerInvite(const CallInvite &invite)
{
stopRingtone();
std::string errorMessage;
if (!session_.init(&errorMessage)) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
hangUp();
return;
}
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
session_.setTurnServers(turnURIs_);
if (!session_.acceptOffer(invite.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
return;
}
session_.acceptICECandidates(remoteICECandidates_);
remoteICECandidates_.clear();
}
void
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
{
if (callCandidatesEvent.sender == utils::localUser().toStdString())
return;
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
callCandidatesEvent.content.call_id,
callCandidatesEvent.sender);
if (callid_ == callCandidatesEvent.content.call_id) {
if (onActiveCall())
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
else {
// CallInvite has been received and we're awaiting localUser to accept or
// reject the call
for (const auto &c : callCandidatesEvent.content.candidates)
remoteICECandidates_.push_back(c);
}
}
}
void
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
{
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
callAnswerEvent.content.call_id,
callAnswerEvent.sender);
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
callid_ == callAnswerEvent.content.call_id) {
emit ChatPage::instance()->showNotification("Call answered on another device.");
stopRingtone();
MainWindow::instance()->hideOverlay();
return;
}
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
}
}
}
void
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
{
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
callHangUpEvent.content.call_id,
callHangUpReasonString(callHangUpEvent.content.reason),
callHangUpEvent.sender);
if (callid_ == callHangUpEvent.content.call_id) {
MainWindow::instance()->hideOverlay();
endCall();
}
}
void
CallManager::generateCallID()
{
using namespace std::chrono;
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
callid_ = "c" + std::to_string(ms);
}
void
CallManager::clear()
{
roomid_.clear();
callPartyName_.clear();
callPartyAvatarUrl_.clear();
callid_.clear();
remoteICECandidates_.clear();
}
void
CallManager::endCall()
{
stopRingtone();
clear();
session_.end();
}
void
CallManager::refreshTurnServer()
{
turnURIs_.clear();
turnServerTimer_.start(2000);
}
void
CallManager::retrieveTurnServer()
{
http::client()->get_turn_server(
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
if (err) {
turnServerTimer_.setInterval(5000);
return;
}
emit turnServerRetrieved(res);
});
}
void
CallManager::playRingtone(const QString &ringtone, bool repeat)
{
static QMediaPlaylist playlist;
playlist.clear();
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
: QMediaPlaylist::CurrentItemOnce);
playlist.addMedia(QUrl(ringtone));
player_.setVolume(100);
player_.setPlaylist(&playlist);
}
void
CallManager::stopRingtone()
{
player_.setPlaylist(nullptr);
}
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer)
{
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
// where username and password are percent-encoded
std::vector<std::string> ret;
for (const auto &uri : turnServer.uris) {
if (auto c = uri.find(':'); c == std::string::npos) {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
} else {
std::string scheme = std::string(uri, 0, c);
if (scheme != "turn" && scheme != "turns") {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
QString encodedUri =
QString::fromStdString(scheme) + "://" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
":" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
"@" + QString::fromStdString(std::string(uri, ++c));
ret.push_back(encodedUri.toStdString());
}
}
return ret;
}
}

@ -0,0 +1,76 @@
#pragma once
#include <string>
#include <vector>
#include <QMediaPlayer>
#include <QObject>
#include <QSharedPointer>
#include <QString>
#include <QTimer>
#include "mtx/events/collections.hpp"
#include "mtx/events/voip.hpp"
namespace mtx::responses {
struct TurnServer;
}
class UserSettings;
class WebRTCSession;
class CallManager : public QObject
{
Q_OBJECT
public:
CallManager(QSharedPointer<UserSettings>);
void sendInvite(const QString &roomid);
void hangUp(
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
bool onActiveCall() const;
QString callPartyName() const { return callPartyName_; }
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
void refreshTurnServer();
public slots:
void syncEvent(const mtx::events::collections::TimelineEvents &event);
signals:
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
void newCallParty();
void turnServerRetrieved(const mtx::responses::TurnServer &);
private slots:
void retrieveTurnServer();
private:
WebRTCSession &session_;
QString roomid_;
QString callPartyName_;
QString callPartyAvatarUrl_;
std::string callid_;
const uint32_t timeoutms_ = 120000;
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
std::vector<std::string> turnURIs_;
QTimer turnServerTimer_;
QSharedPointer<UserSettings> settings_;
QMediaPlayer player_;
template<typename T>
bool handleEvent_(const mtx::events::collections::TimelineEvents &event);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
void answerInvite(const mtx::events::msg::CallInvite &);
void generateCallID();
void clear();
void endCall();
void playRingtone(const QString &ringtone, bool repeat);
void stopRingtone();
};

File diff suppressed because it is too large Load Diff

@ -19,6 +19,7 @@
#include <atomic>
#include <optional>
#include <stack>
#include <variant>
#include <mtx/common.hpp>
@ -34,7 +35,9 @@
#include <QTimer>
#include <QWidget>
#include "CacheCryptoStructs.h"
#include "CacheStructs.h"
#include "CallManager.h"
#include "CommunitiesList.h"
#include "Utils.h"
#include "notifications/Manager.h"
@ -47,10 +50,10 @@ class SideBarActions;
class Splitter;
class TextInputWidget;
class TimelineViewManager;
class TopRoomBar;
class UserInfoWidget;
class UserSettings;
class NotificationsManager;
class TimelineModel;
constexpr int CONSENSUS_TIMEOUT = 1000;
constexpr int SHOW_CONTENT_TIMEOUT = 3000;
@ -77,14 +80,23 @@ public:
QSharedPointer<UserSettings> userSettings() { return userSettings_; }
void deleteConfigs();
CommunitiesList *communitiesList() { return communitiesList_; }
//! Calculate the width of the message timeline.
uint64_t timelineWidth();
bool isSideBarExpanded();
//! Hide the room & group list (if it was visible).
void hideSideBars();
//! Show the room/group list (if it was visible).
void showSideBars();
void initiateLogout();
void query_keys(const std::string &req,
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
void focusMessageInput();
QString status() const;
void setStatus(const QString &status);
mtx::presence::PresenceState currentPresence() const;
public slots:
void leaveRoom(const QString &room_id);
@ -99,8 +111,6 @@ signals:
void connectionLost();
void connectionRestored();
void messageReply(const RelatedInfo &related);
void notificationsRetrieved(const mtx::responses::Notifications &);
void highlightedNotifsRetrieved(const mtx::responses::Notifications &,
const QPoint widgetPos);
@ -114,7 +124,7 @@ signals:
const QString &mime,
qint64 dsize,
const QSize &dimensions,
const std::optional<RelatedInfo> &related);
const QString &blurhash);
void contentLoaded();
void closing();
@ -133,6 +143,7 @@ signals:
void trySyncCb();
void tryDelayedSyncCb();
void tryInitialSyncCb();
void newSyncResponse(mtx::responses::Sync res);
void leftRoom(const QString &room_id);
void initializeRoomList(QMap<QString, RoomInfo>);
@ -142,7 +153,6 @@ signals:
void syncUI(const mtx::responses::Rooms &rooms);
void syncRoomlist(const std::map<QString, RoomInfo> &updates);
void syncTags(const std::map<QString, RoomInfo> &updates);
void syncTopBar(const std::map<QString, RoomInfo> &updates);
void dropToLoginPageCb(const QString &msg);
void notifyMessage(const QString &roomid,
@ -153,18 +163,37 @@ signals:
const QImage &icon);
void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
void themeChanged();
void decryptSidebarChanged();
//! Signals for device verificaiton
void receivedDeviceVerificationAccept(
const mtx::events::msg::KeyVerificationAccept &message);
void receivedDeviceVerificationRequest(
const mtx::events::msg::KeyVerificationRequest &message,
std::string sender);
void receivedRoomDeviceVerificationRequest(
const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message,
TimelineModel *model);
void receivedDeviceVerificationCancel(
const mtx::events::msg::KeyVerificationCancel &message);
void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
std::string sender);
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
private slots:
void showUnreadMessageNotification(int count);
void updateTopBarAvatar(const QString &roomid, const QString &img);
void changeTopRoomInfo(const QString &room_id);
void logout();
void removeRoom(const QString &room_id);
void dropToLoginPage(const QString &msg);
void joinRoom(const QString &room);
void sendTypingNotifications();
void handleSyncResponse(mtx::responses::Sync res);
private:
static ChatPage *instance_;
@ -204,10 +233,13 @@ private:
uint16_t notification_count,
uint16_t highlight_count);
//! Send desktop notification for the received messages.
void sendDesktopNotifications(const mtx::responses::Notifications &);
void sendNotifications(const mtx::responses::Notifications &);
void showNotificationsDialog(const QPoint &point);
template<typename T>
void connectCallMessage();
QHBoxLayout *topLayout_;
Splitter *splitter;
@ -225,7 +257,6 @@ private:
TimelineViewManager *view_manager_;
SideBarActions *sidebarActions_;
TopRoomBar *top_bar_;
TextInputWidget *text_input_;
QTimer connectivityTimer_;
@ -244,6 +275,7 @@ private:
QSharedPointer<UserSettings> userSettings_;
NotificationsManager notificationsManager;
CallManager callManager_;
};
template<class Collection>

@ -257,6 +257,18 @@ CommunitiesList::roomList(const QString &id) const
return {};
}
std::vector<std::string>
CommunitiesList::currentTags() const
{
std::vector<std::string> tags;
for (auto &entry : communities_) {
CommunitiesListItem *item = entry.second.data();
if (item->is_tag())
tags.push_back(entry.first.mid(4).toStdString());
}
return tags;
}
void
CommunitiesList::sortEntries()
{

@ -28,6 +28,7 @@ public:
void syncTags(const std::map<QString, RoomInfo> &info);
void setTagsForRoom(const QString &id, const std::vector<std::string> &tags);
std::vector<std::string> currentTags() const;
signals:
void communityChanged(const QString &id);

@ -137,6 +137,8 @@ CommunitiesListItem::updateTooltip()
setToolTip(tr("Favourite rooms"));
else if (tag == "m.lowpriority")
setToolTip(tr("Low priority rooms"));
else if (tag == "m.server_notice")
setToolTip(tr("Server Notices", "Tag translation for m.server_notice"));
else if (tag.startsWith("u."))
setToolTip(tag.right(tag.size() - 2) + tr(" (tag)"));
else

@ -7,7 +7,6 @@
#include "ui/Theme.h"
class RippleOverlay;
class QPainter;
class QMouseEvent;
class CommunitiesListItem : public QWidget

@ -0,0 +1,20 @@
#pragma once
// Class for showing a limited amount of completions at a time
#include <QSortFilterProxyModel>
class CompletionModel : public QSortFilterProxyModel
{
public:
CompletionModel(QAbstractItemModel *model, QObject *parent = nullptr)
: QSortFilterProxyModel(parent)
{
setSourceModel(model);
}
int rowCount(const QModelIndex &parent) const override
{
auto row_count = QSortFilterProxyModel::rowCount(parent);
return (row_count < 7) ? row_count : 7;
}
};

@ -55,7 +55,7 @@ const QRegularExpression url_regex(
// match an URL, that is not quoted, i.e.
// vvvvvv match quote via negative lookahead/lookbehind vv
// vvvv atomic match url -> fail if there is a " before or after vvv
R"((?<!")(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!"))");
R"((?<!["'])(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!["']))");
}
// Window geometry.

@ -0,0 +1,794 @@
#include "DeviceVerificationFlow.h"
#include "Cache.h"
#include "ChatPage.h"
#include "Logging.h"
#include "timeline/TimelineModel.h"
#include <QDateTime>
#include <QTimer>
#include <iostream>
static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
namespace msgs = mtx::events::msg;
static mtx::events::msg::KeyVerificationMac
key_verification_mac(mtx::crypto::SAS *sas,
mtx::identifiers::User sender,
const std::string &senderDevice,
mtx::identifiers::User receiver,
const std::string &receiverDevice,
const std::string &transactionId,
std::map<std::string, std::string> keys);
DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
DeviceVerificationFlow::Type flow_type,
TimelineModel *model,
QString userID,
QString deviceId_)
: sender(false)
, type(flow_type)
, deviceId(deviceId_)
, model_(model)
{
timeout = new QTimer(this);
timeout->setSingleShot(true);
this->sas = olm::client()->sas_init();
this->isMacVerified = false;
auto user_id = userID.toStdString();
this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id);
ChatPage::instance()->query_keys(
user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
err->matrix_error.errcode,
static_cast<int>(err->status_code));
return;
}
if (!this->deviceId.isEmpty() &&
(res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
nhlog::net()->warn("no devices retrieved {}", user_id);
return;
}
this->their_keys = res;
});
ChatPage::instance()->query_keys(
http::client()->user_id().to_string(),
[this](const UserKeyCache &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
err->matrix_error.errcode,
static_cast<int>(err->status_code));
return;
}
if (res.master_keys.keys.empty())
return;
if (auto status =
cache::verificationStatus(http::client()->user_id().to_string());
status && status->user_verified)
this->our_trusted_master_key = res.master_keys.keys.begin()->second;
});
if (model) {
connect(this->model_,
&TimelineModel::updateFlowEventId,
this,
[this](std::string event_id_) {
this->relation.rel_type = mtx::common::RelationType::Reference;
this->relation.event_id = event_id_;
this->transaction_id = event_id_;
});
}
connect(timeout, &QTimer::timeout, this, [this]() {
if (state_ != Success && state_ != Failed)
this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationStart,
this,
&DeviceVerificationFlow::handleStartMessage);
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationAccept,
this,
[this](const mtx::events::msg::KeyVerificationAccept &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
(msg.hash == "sha256") &&
(msg.message_authentication_code == "hkdf-hmac-sha256")) {
this->commitment = msg.commitment;
if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Emoji) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Emoji;
} else {
this->method = mtx::events::msg::SASMethods::Decimal;
}
this->mac_method = msg.message_authentication_code;
this->sendVerificationKey();
} else {
this->cancelVerification(
DeviceVerificationFlow::Error::UnknownMethod);
}
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationCancel,
this,
[this](const mtx::events::msg::KeyVerificationCancel &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
error_ = User;
emit errorChanged();
setState(Failed);
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationKey,
this,
[this](const mtx::events::msg::KeyVerificationKey &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
if (sender) {
if (state_ != WaitingForOtherToAccept) {
this->cancelVerification(OutOfOrder);
return;
}
} else {
if (state_ != WaitingForKeys) {
this->cancelVerification(OutOfOrder);
return;
}
}
this->sas->set_their_key(msg.key);
std::string info;
if (this->sender == true) {
info = "MATRIX_KEY_VERIFICATION_SAS|" +
http::client()->user_id().to_string() + "|" +
http::client()->device_id() + "|" + this->sas->public_key() +
"|" + this->toClient.to_string() + "|" +
this->deviceId.toStdString() + "|" + msg.key + "|" +
this->transaction_id;
} else {
info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
"|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
http::client()->user_id().to_string() + "|" +
http::client()->device_id() + "|" + this->sas->public_key() +
"|" + this->transaction_id;
}
nhlog::ui()->info("Info is: '{}'", info);
if (this->sender == false) {
this->sendVerificationKey();
} else {
if (this->commitment !=
mtx::crypto::bin2base64_unpadded(
mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
this->cancelVerification(
DeviceVerificationFlow::Error::MismatchedCommitment);
return;
}
}
if (this->method == mtx::events::msg::SASMethods::Emoji) {
this->sasList = this->sas->generate_bytes_emoji(info);
setState(CompareEmoji);
} else if (this->method == mtx::events::msg::SASMethods::Decimal) {
this->sasList = this->sas->generate_bytes_decimal(info);
setState(CompareNumber);
}
});
connect(
ChatPage::instance(),
&ChatPage::receivedDeviceVerificationMac,
this,
[this](const mtx::events::msg::KeyVerificationMac &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
std::map<std::string, std::string> key_list;
std::string key_string;
for (const auto &mac : msg.mac) {
for (const auto &[deviceid, key] : their_keys.device_keys) {
(void)deviceid;
if (key.keys.count(mac.first))
key_list[mac.first] = key.keys.at(mac.first);
}
if (their_keys.master_keys.keys.count(mac.first))
key_list[mac.first] = their_keys.master_keys.keys[mac.first];
if (their_keys.user_signing_keys.keys.count(mac.first))
key_list[mac.first] =
their_keys.user_signing_keys.keys[mac.first];
if (their_keys.self_signing_keys.keys.count(mac.first))
key_list[mac.first] =
their_keys.self_signing_keys.keys[mac.first];
}
auto macs = key_verification_mac(sas.get(),
toClient,
this->deviceId.toStdString(),
http::client()->user_id(),
http::client()->device_id(),
this->transaction_id,
key_list);
for (const auto &[key, mac] : macs.mac) {
if (mac != msg.mac.at(key)) {
this->cancelVerification(
DeviceVerificationFlow::Error::KeyMismatch);
return;
}
}
if (msg.keys == macs.keys) {
mtx::requests::KeySignaturesUpload req;
if (utils::localUser().toStdString() == this->toClient.to_string()) {
// self verification, sign master key with device key, if we
// verified it
for (const auto &mac : msg.mac) {
if (their_keys.master_keys.keys.count(mac.first)) {
json j = their_keys.master_keys;
j.erase("signatures");
j.erase("unsigned");
mtx::crypto::CrossSigningKeys master_key = j;
master_key
.signatures[utils::localUser().toStdString()]
["ed25519:" +
http::client()->device_id()] =
olm::client()->sign_message(j.dump());
req.signatures[utils::localUser().toStdString()]
[master_key.keys.at(mac.first)] =
master_key;
}
}
// TODO(Nico): Sign their device key with self signing key
} else {
// TODO(Nico): Sign their master key with user signing key
}
if (!req.signatures.empty()) {
http::client()->keys_signatures_upload(
req,
[](const mtx::responses::KeySignaturesUpload &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"failed to upload signatures: {},{}",
err->matrix_error.errcode,
static_cast<int>(err->status_code));
}
for (const auto &[user_id, tmp] : res.errors)
for (const auto &[key_id, e] : tmp)
nhlog::net()->error(
"signature error for user {} and key "
"id {}: {}, {}",
user_id,
key_id,
e.errcode,
e.error);
});
}
this->isMacVerified = true;
this->acceptDevice();
} else {
this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
}
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationReady,
this,
[this](const mtx::events::msg::KeyVerificationReady &msg) {
if (!sender) {
if (msg.from_device != http::client()->device_id()) {
error_ = User;
emit errorChanged();
setState(Failed);
}
return;
}
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if ((msg.relates_to.has_value() && sender)) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
else {
this->deviceId = QString::fromStdString(msg.from_device);
}
}
this->startVerificationRequest();
});
connect(ChatPage::instance(),
&ChatPage::receivedDeviceVerificationDone,
this,
[this](const mtx::events::msg::KeyVerificationDone &msg) {
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
nhlog::ui()->info("Flow done on other side");
});
timeout->start(TIMEOUT);
}
QString
DeviceVerificationFlow::state()
{
switch (state_) {
case PromptStartVerification:
return "PromptStartVerification";
case CompareEmoji:
return "CompareEmoji";
case CompareNumber:
return "CompareNumber";
case WaitingForKeys:
return "WaitingForKeys";
case WaitingForOtherToAccept:
return "WaitingForOtherToAccept";
case WaitingForMac:
return "WaitingForMac";
case Success:
return "Success";
case Failed:
return "Failed";
default:
return "";
}
}
void
DeviceVerificationFlow::next()
{
if (sender) {
switch (state_) {
case PromptStartVerification:
sendVerificationRequest();
break;
case CompareEmoji:
case CompareNumber:
sendVerificationMac();
break;
case WaitingForKeys:
case WaitingForOtherToAccept:
case WaitingForMac:
case Success:
case Failed:
nhlog::db()->error("verification: Invalid state transition!");
break;
}
} else {
switch (state_) {
case PromptStartVerification:
if (canonical_json.is_null())
sendVerificationReady();
else // legacy path without request and ready
acceptVerificationRequest();
break;
case CompareEmoji:
[[fallthrough]];
case CompareNumber:
sendVerificationMac();
break;
case WaitingForKeys:
case WaitingForOtherToAccept:
case WaitingForMac:
case Success:
case Failed:
nhlog::db()->error("verification: Invalid state transition!");
break;
}
}
}
QString
DeviceVerificationFlow::getUserId()
{
return QString::fromStdString(this->toClient.to_string());
}
QString
DeviceVerificationFlow::getDeviceId()
{
return this->deviceId;
}
bool
DeviceVerificationFlow::getSender()
{
return this->sender;
}
std::vector<int>
DeviceVerificationFlow::getSasList()
{
return this->sasList;
}
void
DeviceVerificationFlow::setEventId(std::string event_id_)
{
this->relation.rel_type = mtx::common::RelationType::Reference;
this->relation.event_id = event_id_;
this->transaction_id = event_id_;
}
void
DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
std::string)
{
if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id)
return;
} else if (msg.relates_to.has_value()) {
if (msg.relates_to.value().event_id != this->relation.event_id)
return;
}
if ((std::find(msg.key_agreement_protocols.begin(),
msg.key_agreement_protocols.end(),
"curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
(std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
(std::find(msg.message_authentication_codes.begin(),
msg.message_authentication_codes.end(),
"hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Emoji) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Emoji;
} else if (std::find(msg.short_authentication_string.begin(),
msg.short_authentication_string.end(),
mtx::events::msg::SASMethods::Decimal) !=
msg.short_authentication_string.end()) {
this->method = mtx::events::msg::SASMethods::Decimal;
} else {
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
return;
}
if (!sender)
this->canonical_json = nlohmann::json(msg);
else {
if (utils::localUser().toStdString() < this->toClient.to_string()) {
this->canonical_json = nlohmann::json(msg);
}
}
if (state_ != PromptStartVerification)
this->acceptVerificationRequest();
} else {
this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
}
}
//! accepts a verification
void
DeviceVerificationFlow::acceptVerificationRequest()
{
mtx::events::msg::KeyVerificationAccept req;
req.method = mtx::events::msg::VerificationMethods::SASv1;
req.key_agreement_protocol = "curve25519-hkdf-sha256";
req.hash = "sha256";
req.message_authentication_code = "hkdf-hmac-sha256";
if (this->method == mtx::events::msg::SASMethods::Emoji)
req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
else if (this->method == mtx::events::msg::SASMethods::Decimal)
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
req.commitment = mtx::crypto::bin2base64_unpadded(
mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
send(req);
setState(WaitingForKeys);
}
//! responds verification request
void
DeviceVerificationFlow::sendVerificationReady()
{
mtx::events::msg::KeyVerificationReady req;
req.from_device = http::client()->device_id();
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
send(req);
setState(WaitingForKeys);
}
//! accepts a verification
void
DeviceVerificationFlow::sendVerificationDone()
{
mtx::events::msg::KeyVerificationDone req;
send(req);
}
//! starts the verification flow
void
DeviceVerificationFlow::startVerificationRequest()
{
mtx::events::msg::KeyVerificationStart req;
req.from_device = http::client()->device_id();
req.method = mtx::events::msg::VerificationMethods::SASv1;
req.key_agreement_protocols = {"curve25519-hkdf-sha256"};
req.hashes = {"sha256"};
req.message_authentication_codes = {"hkdf-hmac-sha256"};
req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal,
mtx::events::msg::SASMethods::Emoji};
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body;
req.transaction_id = this->transaction_id;
this->canonical_json = nlohmann::json(req);
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.relates_to = this->relation;
this->canonical_json = nlohmann::json(req);
}
send(req);
setState(WaitingForOtherToAccept);
}
//! sends a verification request
void
DeviceVerificationFlow::sendVerificationRequest()
{
mtx::events::msg::KeyVerificationRequest req;
req.from_device = http::client()->device_id();
req.methods = {mtx::events::msg::VerificationMethods::SASv1};
if (this->type == DeviceVerificationFlow::Type::ToDevice) {
QDateTime currentTime = QDateTime::currentDateTimeUtc();
req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.to = this->toClient.to_string();
req.msgtype = "m.key.verification.request";
req.body = "User is requesting to verify keys with you. However, your client does "
"not support this method, so you will need to use the legacy method of "
"key verification.";
}
send(req);
setState(WaitingForOtherToAccept);
}
//! cancels a verification flow
void
DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
{
mtx::events::msg::KeyVerificationCancel req;
if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
req.code = "m.unknown_method";
req.reason = "unknown method received";
} else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
req.code = "m.mismatched_commitment";
req.reason = "commitment didn't match";
} else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
req.code = "m.mismatched_sas";
req.reason = "sas didn't match";
} else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
req.code = "m.key_match";
req.reason = "keys did not match";
} else if (error_code == DeviceVerificationFlow::Error::Timeout) {
req.code = "m.timeout";
req.reason = "timed out";
} else if (error_code == DeviceVerificationFlow::Error::User) {
req.code = "m.user";
req.reason = "user cancelled the verification";
} else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
req.code = "m.unexpected_message";
req.reason = "received messages out of order";
}
this->error_ = error_code;
emit errorChanged();
this->setState(Failed);
send(req);
}
//! sends the verification key
void
DeviceVerificationFlow::sendVerificationKey()
{
mtx::events::msg::KeyVerificationKey req;
req.key = this->sas->public_key();
send(req);
}
mtx::events::msg::KeyVerificationMac
key_verification_mac(mtx::crypto::SAS *sas,
mtx::identifiers::User sender,
const std::string &senderDevice,
mtx::identifiers::User receiver,
const std::string &receiverDevice,
const std::string &transactionId,
std::map<std::string, std::string> keys)
{
mtx::events::msg::KeyVerificationMac req;
std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
receiver.to_string() + receiverDevice + transactionId;
std::string key_list;
bool first = true;
for (const auto &[key_id, key] : keys) {
req.mac[key_id] = sas->calculate_mac(key, info + key_id);
if (!first)
key_list += ",";
key_list += key_id;
first = false;
}
req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
return req;
}
//! sends the mac of the keys
void
DeviceVerificationFlow::sendVerificationMac()
{
std::map<std::string, std::string> key_list;
key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
// send our master key, if we trust it
if (!this->our_trusted_master_key.empty())
key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
mtx::events::msg::KeyVerificationMac req =
key_verification_mac(sas.get(),
http::client()->user_id(),
http::client()->device_id(),
this->toClient,
this->deviceId.toStdString(),
this->transaction_id,
key_list);
send(req);
setState(WaitingForMac);
acceptDevice();
}
//! Completes the verification flow
void
DeviceVerificationFlow::acceptDevice()
{
if (!isMacVerified) {
setState(WaitingForMac);
} else if (state_ == WaitingForMac) {
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
this->sendVerificationDone();
setState(Success);
}
}
void
DeviceVerificationFlow::unverify()
{
cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
emit refreshProfile();
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
TimelineModel *timelineModel_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString event_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_,
Type::RoomMsg,
timelineModel_,
other_user_,
QString::fromStdString(msg.from_device)));
flow->setEventId(event_id_.toStdString());
if (std::find(msg.methods.begin(),
msg.methods.end(),
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
flow->cancelVerification(UnknownMethod);
}
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
const mtx::events::msg::KeyVerificationRequest &msg,
QString other_user_,
QString txn_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
flow->transaction_id = txn_id_.toStdString();
if (std::find(msg.methods.begin(),
msg.methods.end(),
mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
flow->cancelVerification(UnknownMethod);
}
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
const mtx::events::msg::KeyVerificationStart &msg,
QString other_user_,
QString txn_id_)
{
QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow(
parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
flow->transaction_id = txn_id_.toStdString();
flow->handleStartMessage(msg, "");
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
TimelineModel *timelineModel_,
QString userid)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
flow->sender = true;
return flow;
}
QSharedPointer<DeviceVerificationFlow>
DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
{
QSharedPointer<DeviceVerificationFlow> flow(
new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
flow->sender = true;
flow->transaction_id = http::client()->generate_txn_id();
return flow;
}

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

Loading…
Cancel
Save