230 Commits

Author SHA1 Message Date
837ed0106f Bumped version to v0.2-beta 2020-09-27 17:13:38 +02:00
4a0bcf9762 Sanitized Issue Proposals (#58)
Fixes #53

Co-authored-by: kske <kai@kske.dev>
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/58
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-09-27 17:02:24 +02:00
829e94fa5f Fixed Bug Regarding Incorrect Pending MessageStatuses in LoginCredentialsProcessor (#61)
Merge branch 'develop' into b/fixing_message_bugs

fixed not receiving pending messageStatus bug

Co-authored-by: delvh <leon@kske.dev>
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/61
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2020-09-27 15:55:59 +02:00
c7ee545ee2 Merge pull request 'Add Ability to Logout' (#50) from f/logout into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/50
Reviewed-by: kske <kai@kske.dev>
2020-09-27 15:48:12 +02:00
d70a848ef3 Merge branch 'develop' into f/logout
Conflicts:
	client/src/main/java/envoy/client/data/CacheMap.java
	client/src/main/java/envoy/client/data/commands/SystemCommandsMap.java
	client/src/main/java/envoy/client/net/Client.java
	client/src/main/java/envoy/client/ui/Startup.java
	client/src/main/java/envoy/client/ui/StatusTrayIcon.java
	client/src/main/java/envoy/client/ui/controller/ChatScene.java
	client/src/main/java/envoy/client/ui/controller/ContactSearchTab.java
2020-09-27 15:27:11 +02:00
d1d52468bc Merge pull request 'Refactoring' (#55) from refactoring into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/55
Reviewed-by: delvh <leon@kske.dev>
2020-09-27 12:06:37 +02:00
ede50ed3e5 Fix Javadoc errors spotted by @delvh 2020-09-27 12:06:01 +02:00
61fbeda05e Applied suggestions from @kske 2020-09-26 21:38:31 +02:00
5daff3620e Update UI on user status change 2020-09-26 12:10:22 +02:00
618a4aa3cf Merge remote-tracking branch 'origin/develop' into f/logout
Conflicts:
	client/src/main/java/envoy/client/ui/controller/ContactSearchTab.java
2020-09-25 23:16:25 +02:00
108db1ae11 Fixed bug not re-performing handshake on logout
Fixes #31
2020-09-25 23:11:30 +02:00
6d7afbaa8f Use ObservableList in LocalDB and Chat, reduce amount of UI refreshes 2020-09-25 19:19:54 +02:00
86e189a40a Dispatch received events to the event bus by default 2020-09-25 16:03:15 +02:00
0efd1e5594 Fold client receivers into event handlers 2020-09-25 15:56:08 +02:00
f6eeeee79b Remove message and event processors from client 2020-09-25 15:28:14 +02:00
8eb7743057 Remove Javadoc header from all source files
Also removed SendEvent and simplified some other calls.
2020-09-25 14:29:23 +02:00
f0e645c0ae Fix Unread Messages Not Being Displayed for Groups (#49)
Fix unread messages not being displayed for groups

Fixes #48
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/49
Reviewed-by: delvh <leon@kske.dev>
2020-09-25 11:22:59 +02:00
af219274f5 Improved logout mechanism a bit, still pretty buggy
(and fixed some inconsistencies)
2020-09-24 18:18:41 +02:00
05d4917bb2 Added key shortcuts and system commands for logout, exit and settings
Additionally added **buggy** logout mechanism: LocalDB is not reset
properly and IndexOutOfBoundsExceptions occur in the UI
2020-09-23 23:11:32 +02:00
f02b01291b Merge pull request 'Fixed Bug not Updating GroupCreationTab After a new Contact was Added' (#46) from b/group-tab-update into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/46
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: kske <kai@kske.dev>
2020-09-23 21:48:43 +02:00
84d80982e5 Merge branch 'develop' into b/group-tab-update 2020-09-23 21:44:41 +02:00
2d9283551a Improved SystemCommand mechanism, added Alert- and ShutdownHelper, and
... added askForConfirmation option
2020-09-23 17:03:32 +02:00
758e52e030 Store the Local Database Inside a Server-Specific Subdirectory (#45)
Store the local database inside a server-specific subdirectory
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/45
Reviewed-by: delvh <leon@kske.dev>
2020-09-23 16:23:42 +02:00
b9e19d69b9 Merge Local Database and Home Directory (#44)
Merge local database and home directory

Fixes #43
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/44
Reviewed-by: delvh <leon@kske.dev>
2020-09-23 16:00:53 +02:00
c6819e637b Fixed bug not updating GroupCreationTab after a new contact was added
Fixes #35
2020-09-22 17:51:33 +02:00
41f07dc452 Fixed Transactions not Getting Closed on the Server (#42)
Fixes #16
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/42
Reviewed-by: kske <kai@kske.dev>
2020-09-22 17:02:50 +02:00
9419ba2ee8 Merge pull request 'Add a LocalDB Auto Save Mechanism' (#41) from f/localdb-autosave into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/41
Reviewed-by: delvh <leon@kske.dev>
2020-09-22 16:45:58 +02:00
f36f330c81 Add a LocalDB auto save mechanism
During startup, a timer is initialized inside the LocalDB which saves it
after 500 milliseconds during startup and then in intervals of 2
minutes, which can be configured in the ClientConfig.
2020-09-22 16:37:43 +02:00
5b4f2762e5 Fix synchronization when initializing user storage 2020-09-22 16:06:19 +02:00
1b60ab3f0d Fixed Bug Not Saving Values When Exiting via “Control”+”Q” (#40)
Fixed bug not saving values when exiting via "Control"+"Q"
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/40
Reviewed-by: kske <kai@kske.dev>
2020-09-22 14:42:51 +02:00
8ed6faca96 Merge pull request 'Make LocalDB Thread Safe and Simplify its API' (#38) from refactor-local-db into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/38
Reviewed-by: delvh <leon@kske.dev>
2020-09-21 20:54:29 +02:00
52d6282e13 Merge branch 'develop' into refactor-local-db 2020-09-21 20:52:41 +02:00
0dbd15e958 Made not-hide_on_close the default option (#39)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/39
Reviewed-by: kske <kai@kske.dev>
2020-09-21 20:52:13 +02:00
d8ae8a65b8 Make LocalDB thread safe and simplify its API 2020-09-21 20:52:01 +02:00
a12d765494 Merge pull request 'Fixed hopefully every bug concerning "enter to send" ability' (#36) from b/message-text-area into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/36
Reviewed-by: kske <kai@kske.dev>
2020-09-21 20:21:46 +02:00
3cd9d76d2c Fixed sudden Eclipse annoyance 2020-09-21 18:49:58 +02:00
d394c2d058 Added option to close Envoy Linux-like with "Control"+"Q" 2020-09-20 22:11:15 +02:00
7cc4928826 Fixed bug removing \n and added ability to use "ctrl"+"enter" for LB
Fixes #34
2020-09-20 16:16:51 +02:00
4959bc9634 Fixed bug not updating UI after click on context menu item
fixes #11
Additionally, previous commit fixes #5
2020-09-20 16:16:44 +02:00
16a0786d54 Fixed bug adding line break in messages sent using "Enter" 2020-09-20 16:16:38 +02:00
40447f3f42 Change Event Bus version to 0.0.4, fix message event handler
The message event handler ignored group messages, as event handlers do
not include subtypes be default. This behavior has been implemented in
Event Bus 0.0.4 and integrated into Envoy.
2020-09-20 14:13:11 +02:00
be945fe3ee Fix threading issue in handshake rejection alert 2020-09-20 09:55:07 +02:00
a8aa1c9ea7 Initialize local database directory during startup 2020-09-20 09:08:09 +02:00
fd21c5789f Add LocalDB Locking
FIxes #32
2020-09-19 15:28:04 +02:00
1ccf4354aa Merge pull request 'Token Based Authentication' (#30) from f/token-based-authentication into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/30
Reviewed-by: delvh <leon@kske.dev>
2020-09-19 14:31:01 +02:00
cb2a3a6540 Remove authentication tokens from logs 2020-09-19 13:43:03 +02:00
3e594c1fbd Handle handshake rejections on invalid token, reuse not expired tokens 2020-09-19 13:33:18 +02:00
f21d077522 Add token-based authentication (without rejection handling) 2020-09-19 11:37:42 +02:00
31cb22035b Add token request to login credentials and "Stay Signed In" checkbox 2020-09-19 09:13:04 +02:00
ec6b67099f Add token to login credentials and database user 2020-09-18 11:29:05 +02:00
89b9afb3db Remove config based autologin
Fixes #27
2020-09-18 10:02:39 +02:00
f98811c899 Update README (#26) 2020-09-16 22:39:35 +02:00
920dcb53fc Update README
Split the README into a project description for users, server admins and programmers. This should be more understandable for users that are not part of the project.
2020-09-16 22:28:56 +02:00
4ba85f68ef Removed MessageCreationEvent and MessageModificationEvent
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/25
Reviewed-by: kske <kai@kske.dev>
2020-09-16 20:52:08 +02:00
e06dd7dd57 Merge branch 'develop' into remove-unused-message-events 2020-09-16 16:05:56 +02:00
c21da25789 Removed MessageCreationEvent and MessageModificationEvent 2020-09-16 15:52:58 +02:00
8a01229855 Merge pull request 'Remove TransientLocalDB and no-db Config Value' (#24) from remove-transient-localdb into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/24
Reviewed-by: delvh <leon@kske.dev>
2020-09-16 15:48:49 +02:00
763830c727 Remove TransientLocalDB and no-db config value 2020-09-16 15:41:00 +02:00
8829f267ec Merge pull request 'Replace the Internal Event Bus with Event Bus 0.0.3' (#20) from integrate-event-bus into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/20
2020-09-09 18:30:31 +02:00
465ed20efa Replace the internal event bus with Event Bus 0.0.3
The Event class has been retrofitted to implement IEvent, so that no
event implementations had to be changed.
2020-09-08 20:41:01 +02:00
69ea737361 Merge pull request 'Restored Compatability with git.kske.dev' (#19) from b/new_vcs into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/19
Reviewed-by: kske <kai@kske.dev>
2020-09-06 15:02:09 +02:00
74a1f8232b Restored compatability with new VCS 2020-09-06 12:17:45 +02:00
9b6d0f3c97 Add top level .settings folder to .gitignore 2020-09-06 11:15:20 +02:00
ff1891108e Remove top level .settings folder 2020-09-06 11:14:37 +02:00
78573399e9 Remove GitHub specific files 2020-09-06 11:12:03 +02:00
beb0f3e469 Merge pull request #42 from informatik-ag-ngl/b/icons_theme_change
Correct icons are loaded when changing the theme.
2020-09-02 11:33:42 +02:00
dd2e09b6dc Apply suggestions from code review from @CyB3RC0nN0R
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-09-02 11:31:21 +02:00
cf401d201c Merge branch 'develop' into b/icons_theme_change 2020-09-02 11:25:57 +02:00
aa992e2bcf Implemented custom preview support on theme change. 2020-09-02 11:10:05 +02:00
63ed1c480d Merge pull request #43 from informatik-ag-ngl/f/context
Simplified dependency injection for the client
2020-09-02 10:39:53 +02:00
3f3c561e25 Apply suggestions from code review
will anyone read this? Bli bla blub. I can write anything here and no one will notice. Bwuhahaha.

Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-09-02 10:37:26 +02:00
fcd5767c4b Reduce wildcard import threshold to 2 2020-09-02 10:32:44 +02:00
d97af36ae1 shrank delay of "offline"-Tooltip in ChatScene 2020-09-02 10:17:50 +02:00
d0c8c685ab Fix LoginScene popping 2020-09-02 10:07:02 +02:00
8b204b3715 Fix write proxy initialization 2020-09-02 09:54:15 +02:00
efbca9cbc9 Fix tab FXML paths 2020-09-02 09:24:46 +02:00
661823219c Removed clicking into a tab to see that you cannot interact with it
Additionally re-ensured compliance with our CSS conventions.
2020-09-01 21:36:23 +02:00
9f517cfc6b Added better dependency injection mechanism and purified LoginScene
one thing could for whatever reason not be avoided: Even though the
processors of the caches inside WriteProxy are initialized, they somehow
get "de-initialized" and have to be initialized again...
2020-09-01 20:14:02 +02:00
ee0d70647c Edited onRestore method in ChatScene. 2020-08-31 19:53:14 +02:00
88f28e60f1 Fixed a minor merging problem that wasn't fixed in the prior branch. 2020-08-31 11:54:20 +02:00
9bd06336eb Fixed bug not allowing users without command line arguments 2020-08-31 09:02:07 +02:00
dc114e5b3c Remove project specific .gitignore files 2020-08-31 08:52:58 +02:00
f6c62f9073 Merge pull request #39 from informatik-ag-ngl/f/finishing_new_UI
Finished new UI by adding missing Components (not Settings screen yet)
2020-08-31 08:10:19 +02:00
4137bf393a Fixed Typo 2020-08-30 19:45:51 +02:00
dc58290f22 Merge branch 'develop' into f/finishing_new_UI 2020-08-30 15:48:29 +02:00
74025c6111 Added Tabs Constant 2020-08-30 15:33:19 +02:00
6c32cf650e Unified color specifications to HEX colors 2020-08-30 15:14:31 +02:00
f86f3ec200 Applied some minor changes requested by @delvh and CyberSomething (I really cant remember how your name is spelled)
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
Co-authored-by: delvh <dev.lh@web.de>
2020-08-29 12:01:51 +02:00
f581b83359 Added semi-proper light theme and fixed some coloring bugs. 2020-08-26 18:31:23 +02:00
b7ea7f0e85 Applied some more suggestions from code review. 2020-08-26 17:53:53 +02:00
e7d85bd968 unified id variable names to kebab-case 2020-08-26 15:50:46 +02:00
15265d2b7c Merge branch 'f/finishing_new_UI' of git@github.com:informatik-ag-ngl/envoy.git into f/finishing_new_UI 2020-08-26 15:12:56 +02:00
78ade078d4 Changed some more things requested by @delvh. 2020-08-26 15:12:03 +02:00
5f3e615641 Applied suggestions from code review from @delvh
Co-authored-by: delvh <dev.lh@web.de>
2020-08-26 14:36:12 +02:00
572541e381 Fixed bug disabling server startup
For further information please take a look at the first Javadoc comment
in envoy.data.Config
2020-08-24 23:20:11 +02:00
f6c3da394d Added Javadoc and removed redundant imports 2020-08-24 21:58:36 +02:00
da309098b7 Added offline mode warning and note 2020-08-24 21:54:25 +02:00
1983cebde1 Shortened code 2020-08-24 21:08:48 +02:00
46a883dda9 Added nice error handling when creating groups insted of alert 2020-08-23 22:29:13 +02:00
a6e5b3d77d Merge pull request #38 from informatik-ag-ngl/f/server_config
Added config for the server and improved config mechanism.
Additionally:
- fixed small bug in both EnvoyLog and envoy.server.Startup
- fixed bug not exiting receiver thread when the server does not send anymore
- added access authorization for issues
- added option to disable attachments and groups on both client and server
- "finalized" every class that allows doing so
2020-08-23 22:13:35 +02:00
ddbf9acd07 Apply suggestions from code review
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-08-23 22:05:50 +02:00
1d03128744 Removed old FXML file 2020-08-23 21:07:38 +02:00
72ffa71d6b Fixed createButton disabling bug 2020-08-23 20:59:25 +02:00
14ccf4ce58 Data initialization in GroupCreationTab works at the right time 2020-08-23 20:26:22 +02:00
bd75da1ab9 implemented groupCreationTab 2020-08-23 20:15:52 +02:00
f77795edb1 Removed alert when adding new user 2020-08-23 17:24:55 +02:00
dbf69c7cc1 Implemented BackButton functionality 2020-08-23 17:11:41 +02:00
d0f125f058 ContactSearchTab UI finished and reimplemented controller 2020-08-23 12:36:43 +02:00
b4397fe2f2 contactSearchTab 2020-08-22 21:50:05 +02:00
1fe83dbcc0 Implemented TabPane and done preparation for internal file loading 2020-08-22 21:02:49 +02:00
c784ebb787 Added option to disable attachments and groups on both client and server 2020-08-22 18:14:26 +02:00
eb4e421974 Made every class that can be final final 2020-08-22 13:51:17 +02:00
4bbc4189ec Updated config mechanism and added config for the server
Additionally fixed a small bug in EnvoyLog and envoy.server.Startup,
fixed Receiver not stopping when the server was stopped
and added access token authorization for the server config
2020-08-22 13:15:42 +02:00
19dcb2bea8 Merge pull request #35 from informatik-ag-ngl/f/b/reporting
Added option to autocreate bug issues on client and server side
2020-08-22 10:53:48 +02:00
2cb124505d Apply suggestions from code review
Additionally moved issue sanitization from server to client.

Co-authored-by: DieGurke <maxi@kske.dev>
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-08-20 13:49:23 +02:00
cb95c40ad6 Initial commit 2020-08-20 11:02:51 +02:00
b081960a31 Merge branch 'f/b/reporting' of git@github.com:informatik-ag-ngl/envoy.git into develop 2020-08-16 22:10:05 +02:00
f4a3bfed97 Added option to autocreate bug issues on client and server side
Additionally cleaned up a few classes a bit
2020-08-16 17:14:41 +02:00
ecede45360 Add install script for developers on Debian-based operating systems 2020-08-15 09:37:16 +02:00
5acbd3b6e1 Merge pull request #34 from informatik-ag-ngl/f/changeable_user
Added ability to change profile pic, username and password
2020-08-03 22:29:03 +02:00
33aa851090 Fix edge case in AbstractListCell
Clear the cell if the item is updated with a null value.
2020-08-03 22:07:12 +02:00
2491812ba0 Apply code review suggestions from @CyB3RC0nN0R 2 2020-08-03 15:10:35 +02:00
71bb329857 Apply code review suggestions from @CyB3RC0nN0R
Additionally added Tooltips to all current items in the SettingsScene,
added ReflectionUtil, changed the cursor on listcells and merged develop
into this branch
2020-08-02 20:26:22 +02:00
dee317c27d Merge pull request #33 from informatik-ag-ngl/f/new_ui
New ChatScene UI
2020-08-02 15:30:06 +02:00
c3dfedc642 Made system commands case insensitive and reworked /dabr mechanism 2020-08-01 21:40:20 +02:00
a1d09d6550 Fixed errors caused by the new ListModel 2020-08-01 17:34:34 +02:00
0901f900e7 Some minor fixes
Co-authored-by: delvh <dev.lh@web.de>
2020-08-01 17:24:15 +02:00
56bb00cd32 Added logging and fixed some security concerns 2020-08-01 14:57:08 +02:00
fe4f9bf219 Replaced shitty javadoc with nice new and young javadoc
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-08-01 11:09:24 +02:00
209262b4c9 Merge branch 'develop' into f/new_ui 2020-08-01 10:49:40 +02:00
3fdbbfd756 redesigned the vertical scroll bar 2020-08-01 10:30:12 +02:00
0d77fbf831 Added ability to change the password, theoretically on client and server
(needs testing!)
2020-08-01 10:17:39 +02:00
59188711b8 Fixed size initialization bug regarding correct computation of scenesize 2020-08-01 10:04:53 +02:00
74ebd158f2 Made the contact search area appealing for the eye and relocated buttons 2020-08-01 10:00:34 +02:00
719aa4cd4f Added profile pic change mechanism on client and common side 2020-08-01 10:00:29 +02:00
498f3ef43d Added ability to change the user name on the client side 2020-08-01 09:54:18 +02:00
b02c2fdc65 Changed SettingsPane mechanism a bit 2020-08-01 09:43:15 +02:00
b678ae295b Fix a casting issue 2020-07-31 22:52:42 +02:00
3cbe3b5045 Merge pull request #31 from informatik-ag-ngl/f/simple_object_processor
Remove ObjectProcessor#getInputClass
2020-07-31 16:49:04 +00:00
268e4439d7 implemented contact search 2020-07-31 18:46:32 +02:00
98ebb321ce Added OOP approach to some boilerplate code currently implemented
@DieGurke,as I don't want to interfere with your branch at all, I only
added the absolute minimum that should be mergeable without conflict.
I leave the rest of the implementation (usage in ChatScene, ChatControl
and referencing in FXML) up to you.
There's no way in hell I'll risk your wrath...
2020-07-30 20:46:28 +02:00
9234e23fae Fixed various bugs
These are:
* different size of addContact- and SettingsButton
* default icons in light mode for users and groups (even though they are
currently just the version used in dark mode)
* wrong preferred size of unnamed "Login" label in LoginScene
* unopenable LoginScene for some OS (Debian)
* white screen when the current scene is switched

Additionally cleaned up code a bit in MessageControl and
LoginScene(.java)
2020-07-29 21:59:55 +02:00
3e7a949be5 Merge pull request #32 from informatik-ag-ngl/f/save_attachment
Added ability to save attachments.
2020-07-28 15:55:05 +02:00
0167af54b0 Apply suggestions from code review
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-07-28 08:53:10 +02:00
517c840487 Added customizable download path and ability to save without FileChooser 2020-07-27 22:52:43 +02:00
e216152e6b Added ability to save attachments 2020-07-27 12:00:49 +02:00
63f42ab8d9 Remove ObjectProcessor#getInputClass
Replace an explicit input class declaration with nasty reflection code.
2020-07-25 17:34:19 +02:00
1cdad2df0b Merge pull request #30 from informatik-ag-ngl/f/is_typing_event
Added IsTyping event on common, server and partially on client side.

Additionally fixed small NullPointerException in ContactSearchScene
2020-07-25 17:17:29 +02:00
5a5e6e2086 Refactored IsWriting to IsTyping 2020-07-25 17:13:50 +02:00
e382a86623 Apply suggestions from code review
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-07-25 16:51:46 +02:00
6f8859c3fd Added IsWriting event on common, server and partially on client side
additionally fixed NullPointerException in ContactSearchScene and typo
in Javadoc

PS: this is the 1000th commit in Envoy! 🥳 🎉
2020-07-25 16:26:13 +02:00
72d1e074f4 Merge pull request #29 from informatik-ag-ngl/b/looping_receiver
Prevent Receiver from looping after connection loss
2020-07-25 13:15:00 +00:00
cd2e739529 Prevent Receiver from looping after connection loss 2020-07-25 15:09:00 +02:00
8d81b76bad Updated issue templates 2020-07-25 10:43:26 +02:00
c34457730f Add smooth padding transition 2020-07-24 14:22:41 +02:00
00fc160550 Adjust message padding immediately 2020-07-24 14:02:53 +02:00
9d7f85c58d Merge pull request #26 from informatik-ag-ngl/f/system_commands
Added system commands ( features: custom argument number, default values, system command builder, ...).
Fixed bug not copying attachment when using copy and send.
2020-07-24 13:54:05 +02:00
4d4de3a27f Update client/src/main/java/envoy/client/ui/controller/ChatScene.java
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-07-24 13:42:39 +02:00
8718596be2 Added default values, SystemCommandBuilder
Additionally removed sending of SystemCommands as messages and added
sorting of recommendations by relevance.
2020-07-24 11:09:05 +02:00
9a947739a6 Merge pull request #27 from informatik-ag-ngl/f/status_tray
Restore Status Tray Functionality
2020-07-24 08:28:54 +00:00
2ffcad9d35 Apply suggestions from code review 2020-07-24 10:26:31 +02:00
59354c403d Integrated the tray icon with the hide on close setting 2020-07-24 09:57:09 +02:00
07fbe3438a Notify about messages when out of focus 2020-07-23 19:20:58 +02:00
2ed30c56cd Iconify stage on close, reopen it with the tray icon 2020-07-23 18:53:36 +02:00
e49d390089 Apply suggestions from code review (1)
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-07-23 18:51:20 +02:00
d3c2eb4ff7 Added SystemCommandsMap in Chatscene and "DABR"-command 2020-07-23 17:18:53 +02:00
42184c47f7 Added onCall interface, InterruptEvent, and a relevance measurement 2020-07-23 16:37:28 +02:00
6a1a9ecdbb Added consistent and safer way to get the currently requested command 2020-07-23 15:50:45 +02:00
f1856534c6 Adjusted formatter to new Envoy version 2020-07-23 15:36:23 +02:00
9ea8d24ab6 Adjusted MessageControl and linked css 2020-07-23 11:11:30 +02:00
38c57c997f Added mechanism to check whether a raw text contains a command 2020-07-23 11:07:54 +02:00
7bf35977f0 Added validity check for commands 2020-07-23 09:23:29 +02:00
5d2a3b83d2 Message Text Line-Wrap works properly now 2020-07-22 11:49:32 +02:00
9e427e1ec3 Adjusted message rendering 2020-07-22 00:03:12 +02:00
ebfe603bc7 Add binding from list width to message padding 2020-07-21 09:01:54 +02:00
60791f2913 Fixed problems with groupMemberName displaying 2020-07-20 14:09:30 +02:00
5d03d0f0eb Make StatusTrayIcon work with JavaFX
* Remove Swing dependencies from StatusTrayIcon
* Pass a stage to the constructor
* Adjust focus change handler and reactivation
* Add IconUtil#loadAWTCompatible for BufferedImage loading
2020-07-20 12:57:34 +02:00
79a121b6b5 Added name displaying of groupMessages in groupChats 2020-07-20 12:32:53 +02:00
e00fa592d6 Merge pull request #24 from informatik-ag-ngl/b/same_time_contact_addition
Fixed bug enabling contact duplication when two clients simultaneously add each other to their contact list
2020-07-19 23:20:53 +02:00
a283217308 Fixed bug enabling contact duplication
...when two clients simultaneously add each other to the respective
contact list
2020-07-18 18:20:52 +02:00
145ec06f57 Added README.md 2020-07-18 15:58:39 +02:00
01f81fadac Fixed resize problems and some other stuff 2020-07-18 14:41:25 +02:00
e51d2946d0 Change artifact directory structure 2020-07-18 14:00:26 +02:00
1a17448724 Speed up build, move compiler configuration to parent POM 2020-07-18 13:32:49 +02:00
0674035183 Reworked list cell framework to be more extensible 2020-07-18 11:50:49 +02:00
fdbec3d652 Merge branch 'develop' into f/system_commands 2020-07-18 11:27:59 +02:00
5ce62c10ca Added System command description and added recommendation ability
Additionally removed ability to decide whether exceptions thrown by
SystemCommands should be rethrown as a mentally superior team member
intervened that it would be useless.
2020-07-18 11:25:41 +02:00
fa7be8c343 Merge branch 'develop' into f/new_ui
Conflicts:
	client/src/main/java/envoy/client/ui/controller/LoginScene.java
2020-07-18 10:49:24 +02:00
282db47153 Reconfigure Envoy Common Eclipse Project 2020-07-18 10:19:47 +02:00
381740e087 Simplify project names 2020-07-18 10:11:46 +02:00
da77afdc32 Fixed bug not copying attachment when using copy and send 2020-07-18 09:48:08 +02:00
2e42da87ec Merge pull request #17 from informatik-ag-ngl/f/handshake_sync
Message Synchronization During Handshake
2020-07-18 07:17:35 +00:00
2e45e375b1 Revised SystemCommand mechanism and implemented theoretical execution 2020-07-17 23:27:54 +02:00
2e4a17c6c5 Fixed scaling problems (especially on lower res displays)
Still a problem with max width of column 1 and max size of stage due to
sizeToScene property on the stage
2020-07-17 17:02:58 +02:00
b4225b0d80 Implemented ProfilePics UI mechanism 2020-07-17 13:56:36 +02:00
f135a99fdd Merge branch 'develop' into f/handshake_sync 2020-07-16 22:35:09 +00:00
698e260746 Turn logging off by default (ClientConfig)
Logging is still enabled through the client.properties however, where
console logging is set to FINER.
2020-07-17 00:29:48 +02:00
47ab5d1e0c Fix unread message counter
A bug remains when the total status of a group message is SENT, but the
individual status for the client user is RECEIVED. In this case, the
counter should be incremented but isn't.
2020-07-17 00:27:00 +02:00
71145bbb24 Added System Commands basics - may change again 2020-07-17 00:23:35 +02:00
62d9df7ae8 Merge branch 'develop' into f/new_ui 2020-07-16 22:11:52 +02:00
b88f260efc Changed color of messageList background 2020-07-16 22:10:04 +02:00
e104a1f9b4 Merge pull request #18 from informatik-ag-ngl/f/listview_refresh
Added (inefficient) listview refreshing mechanism.
Additionally fixed these bugs/ inconsistencies:
    Removed the selected user from ContactSearchScene upon addition
    Warned user on group creation if he already has a Group with that name
    Fixed bug not enabling the post-button when an attachment is present
2020-07-16 22:04:39 +02:00
7b693e0328 Fixed some issues 2020-07-16 21:52:07 +02:00
afcf1e48a4 Remove filter from ReceivedMessageProcessor, improve handshake
The user is sent after the messages to avoid receiving messages on the
client while switching from handshake to normal mode.
2020-07-16 21:14:37 +02:00
a21a5c8588 Improved top bar 2020-07-16 21:13:46 +02:00
00603bedf6 Update client/src/main/java/envoy/client/ui/controller/GroupCreationScene.java
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-07-16 20:54:47 +02:00
96bfe489da Update client/src/main/java/envoy/client/ui/controller/GroupCreationScene.java
Co-authored-by: CyB3RC0nN0R <kske@outlook.de>
2020-07-16 20:54:15 +02:00
698b57d99d Fixed Bug not updating MessageStatusChanges 2020-07-16 20:34:24 +02:00
c71c038317 Fixed styleSheet mess regarding the Lists 2020-07-16 19:07:27 +02:00
43c1edae39 Adjust message queries for handshake sync
This causes problems with group messages as the received date is null
sometimes even though the status is RECEIVED.

The ReceivedMessageProcessor on the client filters out the synced
messages at the moment.
2020-07-16 18:32:40 +02:00
176f6c6463 Fixed bug not enabling the post-button when an attachment is present 2020-07-16 18:23:06 +02:00
bf499da97d Adjusted message Enter bar (field and buttons) and changed color 2020-07-16 18:17:52 +02:00
c0f4a8e212 Warned user on group creation if he already has a Group with that name 2020-07-16 17:47:59 +02:00
fb4fd85fe4 Removed the selected user from ContactSearchScene upon addition 2020-07-16 17:36:57 +02:00
bc355f190f Added deepRefresh - mechanism
additionally fixed bug not updating messageList when a
MessageStatusChange occurs (seriously, why did no one notice it before?)
2020-07-16 17:35:15 +02:00
a76c2a347e Relocated existing Components of chatScene and adjusted them a bit 2020-07-16 17:28:00 +02:00
07c4ccf3c8 Prepare handshake synchronization
Common
* Replace LocalDateTime with Instant everywhere

Client
* Display message creation date with system time zone in MessageControl
* LocalDB#users now strictly contains Users
* lastSync time stamp in LocalDB (saved per user)
* isOnline parameter in save function (lastSync updated if true)
* lastSync time stamp in LoginCredentials
* No ClientConfig#getLoginCredentials because of missing information,
  moved to LoginScene
* Pass LocalDB#lastSync to LoginCredentials in LoginScene

Server
* Explicit lastSync parameter for
  PersistenceManager#getPending(Group)Messages

This sends the correct time stamp to the server, however the JPQL
queries have yet to be adjusted.
2020-07-16 17:04:35 +02:00
e7e4c5af42 Login Scene is not resizable, logo gets loaded correctly 2020-07-16 16:02:03 +02:00
1e63c1a7d1 Persisted really important statement forever in comment 2020-07-15 21:48:06 +02:00
c5094e52cd Fixed bug not scrolling to the correct message 2020-07-15 21:44:57 +02:00
9a9a475c0e Implemented completely new UI for the login scene 2020-07-15 18:45:55 +02:00
f608b2d6ec Replaced custom clearableTextField with normal TextField 2020-07-15 14:05:47 +02:00
abd0113588 Merge pull request #11 from informatik-ag-ngl/f/contact_control
Extract ContactControl from ChatControl + Chat -> User Refactorings
2020-07-14 19:30:15 +00:00
ba336908d1 Add Generic ListViewFactory 2020-07-13 22:08:08 +02:00
4bc393b055 Rename ContactSearchProcessor to UserSearchProcessor 2020-07-13 21:34:21 +02:00
bdd1b40107 Move pull request templates to .github/ 2020-07-13 19:35:17 +02:00
0267a7bbab Fix FXML naming error 2020-07-13 19:16:48 +02:00
a437fb25da Fix FXML formatting 2020-07-13 19:12:03 +02:00
659a468049 Add ContactListCellFactory
- Refactor chatList to userList in ContactSearchScene and
  GroupCreationScene
- Narrow contact searches down to users on a datamodel basis
- Refactor ContactSearchRequest and ContactSearchResult to
  UserSearchRequest and UserSearchResult
2020-07-13 19:02:40 +02:00
062c9f418d Extract ContactControl from ChatControl
The new class ContactControl displays the contact name and status (user)
or member count (group) and is used inside ChatControl, which adds the
unread message count label.
2020-07-13 17:55:00 +02:00
207 changed files with 5996 additions and 3801 deletions

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @CyB3RC0nN0R

View File

@ -1,32 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: CyB3RC0nN0R, delvh, DieGurke, derharry333
projects: Envoy
milestone: Envoy v0.2-beta
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Debian GNU/Linux, Microsoft Windows 10]
- Version [e.g. 0.1-beta]
**Additional context**
Add any other context about the problem here.

View File

@ -1,21 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement, feature
assignees: CyB3RC0nN0R, delvh, DieGurke
project: Envoy
milestones: Envoy v0.2-beta
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,10 +0,0 @@
---
name: Bug fix
title: Fixed Bug
labels: bug
assignees: CyB3RC0nN0R, delvh, DieGurke
reviewers: CyB3RC0nN0R, delvh
projects: Envoy
milestone: Envoy v0.1-beta
---
Fixes #{issue}

View File

@ -1,9 +0,0 @@
---
name: Feature integration
title: Added feature
labels: feature
assignees: CyB3RC0nN0R, delvh, DieGurke
reviewers: CyB3RC0nN0R, delvh
projects: Envoy
milestone: Envoy v0.1-beta
---

View File

@ -1,10 +0,0 @@
---
name: Updated Javadoc
title: Updated Javadoc
labels: documentation
assignees: CyB3RC0nN0R, delvh
reviewers: CyB3RC0nN0R, delvh
projects: Envoy
milestone: Envoy v0.1-beta
---

View File

@ -1,32 +0,0 @@
name: Java CI
on: [push]
jobs:
build:
name: Build & Package
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Build with Maven
run: mvn -B package
- uses: actions/upload-artifact@v2
with:
name: envoy-${{ matrix.os }}
path: |
server/target/envoy-server-jar-with-dependencies.jar
client/target/envoy-client*shaded.jar

7
.gitignore vendored
View File

@ -1 +1,8 @@
# build folders
target/ target/
# Eclipse settings
/.settings
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*.log

View File

@ -1,2 +0,0 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

52
README.md Normal file
View File

@ -0,0 +1,52 @@
# Envoy
<img src="https://git.kske.dev/repo-avatars/33-31e14133097b01b748ab361e3c3adb47" style="display: block" width="150" height="150">
Envoy is a messenger written in Java.
On this page, the project is explained for different user groups.
## Regular User
To use Envoy to join an existing server, download the client from the [release page](https://git.kske.dev/zdm/envoy/releases).
When starting it for the first time, you can register yourself at a server of your choice.
After connecting to the server, you can add other users to your contact list and send them messages.
To chat with multiple users at once, you can create a group.
If you want to transfer a file to another user, you can attach it to a message.
On the settings page some convenience features can be configured, as well as the color theme.
## Server Administrator
To set up an Envoy server, download the package from the release page.
Because the project lacks external documentation for the moment, please refer to the Javadoc inside the source code to configure your Envoy instance.
## Programmer
Envoy is organized as a Maven project that is split into three modules.
### Client
* Sending and receiving of messages, groups, sending images and voice messages
* User interface (UI)
* Client configuration
* Advanced logging possibilities
* Tons of Events to interact with
* Detailed Javadoc to improve readability of code
### Common
* Basic datatypes
* Events sent between client and server
* Configuration API
* Logging API based on `java.util.logging`
* Envoy-specific Exception
* Useful utility classes
### Server
* Non-blocking connectivity infrastructure based on `java.nio`
* Processors to handle incoming events
* Database connectivity
* Databse entities
* Utility classes to check client version compatability and password validity

View File

@ -13,9 +13,9 @@
</classpathentry> </classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"> <classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes> <attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/> <attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">

4
client/.gitignore vendored
View File

@ -1,4 +0,0 @@
/target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<projectDescription> <projectDescription>
<name>envoy-client</name> <name>client</name>
<comment></comment> <comment></comment>
<projects> <projects>
</projects> </projects>

View File

@ -18,6 +18,7 @@ org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.doc.comment.support=enabled org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.APILeak=warning org.eclipse.jdt.core.compiler.problem.APILeak=warning
org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.autoboxing=ignore

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
default.configuration=
eclipse.preferences.version=1
hibernate3.enabled=false

View File

@ -9,14 +9,14 @@
<parent> <parent>
<groupId>informatik-ag-ngl</groupId> <groupId>informatik-ag-ngl</groupId>
<artifactId>envoy</artifactId> <artifactId>envoy</artifactId>
<version>0.1-beta</version> <version>0.2-beta</version>
</parent> </parent>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>informatik-ag-ngl</groupId> <groupId>informatik-ag-ngl</groupId>
<artifactId>envoy-common</artifactId> <artifactId>envoy-common</artifactId>
<version>0.1-beta</version> <version>0.2-beta</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
@ -37,15 +37,6 @@
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
</resource> </resource>
</resources> </resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -9,15 +9,18 @@ import envoy.client.ui.Startup;
* <p> * <p>
* To allow Maven shading, the main method has to be separated from the * To allow Maven shading, the main method has to be separated from the
* {@link Startup} class which extends {@link Application}. * {@link Startup} class which extends {@link Application}.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Main.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class Main { public final class Main {
/**
* A funny debug switch put in by {@code delvh} to enable easy debugging.
*
* @since Envoy Client v0.2-beta
*/
private static final boolean debug = false;
/** /**
* Starts the application. * Starts the application.
@ -26,5 +29,12 @@ public class Main {
* client configuration * client configuration
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static void main(String[] args) { Application.launch(Startup.class, args); } public static void main(String[] args) {
if (debug) {
// Put testing code here
System.out.println();
System.exit(0);
}
Application.launch(Startup.class, args);
}
} }

View File

@ -1,20 +1,14 @@
package envoy.client.data; package envoy.client.data;
import java.io.Serializable; import java.io.Serializable;
import java.util.LinkedList; import java.util.*;
import java.util.Queue;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* Stores elements in a queue to process them later. * Stores elements in a queue to process them later.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Cache.java</strong><br>
* Created: <strong>6 Feb 2020</strong><br>
* *
* @param <T> the type of cached elements * @param <T> the type of cached elements
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -62,4 +56,11 @@ public final class Cache<T> implements Consumer<T>, Serializable {
elements.forEach(processor::accept); elements.forEach(processor::accept);
elements.clear(); elements.clear();
} }
/**
* Clears this cache of all stored elements.
*
* @since Envoy Client v0.2-beta
*/
public void clear() { elements.clear(); }
} }

View File

@ -1,17 +1,12 @@
package envoy.client.data; package envoy.client.data;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.*;
import java.util.Map;
/** /**
* Stores a heterogeneous map of {@link Cache} objects with different type * Stores a heterogeneous map of {@link Cache} objects with different type
* parameters. * parameters.
* <p> *
* Project: <strong>envoy-client</strong><br>
* File: <strong>CacheMap.java</strong><br>
* Created: <strong>09.07.2020</strong><br>
*
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -23,7 +18,7 @@ public final class CacheMap implements Serializable {
/** /**
* Adds a cache to the map. * Adds a cache to the map.
* *
* @param <T> the type accepted by the cache * @param <T> the type accepted by the cache
* @param key the class that maps to the cache * @param key the class that maps to the cache
* @param cache the cache to store * @param cache the cache to store
@ -33,7 +28,7 @@ public final class CacheMap implements Serializable {
/** /**
* Returns a cache mapped by a class. * Returns a cache mapped by a class.
* *
* @param <T> the type accepted by the cache * @param <T> the type accepted by the cache
* @param key the class that maps to the cache * @param key the class that maps to the cache
* @return the cache * @return the cache
@ -43,7 +38,7 @@ public final class CacheMap implements Serializable {
/** /**
* Returns a cache mapped by a class or any of its subclasses. * Returns a cache mapped by a class or any of its subclasses.
* *
* @param <T> the type accepted by the cache * @param <T> the type accepted by the cache
* @param key the class that maps to the cache * @param key the class that maps to the cache
* @return the cache * @return the cache
@ -52,7 +47,7 @@ public final class CacheMap implements Serializable {
public <T> Cache<? super T> getApplicable(Class<T> key) { public <T> Cache<? super T> getApplicable(Class<T> key) {
Cache<? super T> cache = get(key); Cache<? super T> cache = get(key);
if (cache == null) if (cache == null)
for (var e : map.entrySet()) for (final var e : map.entrySet())
if (e.getKey().isAssignableFrom(key)) if (e.getKey().isAssignableFrom(key))
cache = (Cache<? super T>) e.getValue(); cache = (Cache<? super T>) e.getValue();
return cache; return cache;
@ -63,4 +58,11 @@ public final class CacheMap implements Serializable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public Map<Class<?>, Cache<?>> getMap() { return map; } public Map<Class<?>, Cache<?>> getMap() { return map; }
/**
* Clears the caches of this map of any values.
*
* @since Envoy Client v0.2-beta
*/
public void clear() { map.values().forEach(Cache::clear); }
} }

View File

@ -1,10 +1,9 @@
package envoy.client.data; package envoy.client.data;
import java.io.IOException; import java.io.*;
import java.io.Serializable; import java.util.*;
import java.util.ArrayList;
import java.util.List; import javafx.collections.*;
import java.util.Objects;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.*;
@ -14,10 +13,6 @@ import envoy.event.MessageStatusChange;
/** /**
* Represents a chat between two {@link User}s * Represents a chat between two {@link User}s
* as a list of {@link Message} objects. * as a list of {@link Message} objects.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Chat.java</strong><br>
* Created: <strong>19 Oct 2019</strong><br>
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Leon Hofmeister * @author Leon Hofmeister
@ -26,12 +21,18 @@ import envoy.event.MessageStatusChange;
*/ */
public class Chat implements Serializable { public class Chat implements Serializable {
protected final Contact recipient; protected final Contact recipient;
protected final List<Message> messages = new ArrayList<>();
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount; protected int unreadAmount;
private static final long serialVersionUID = 1L; /**
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
*/
protected transient long lastWritingEvent;
private static final long serialVersionUID = 2L;
/** /**
* Provides the list of messages that the recipient receives. * Provides the list of messages that the recipient receives.
@ -41,16 +42,24 @@ public class Chat implements Serializable {
* @param recipient the user who receives the messages * @param recipient the user who receives the messages
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
*/ */
public Chat(Contact recipient) { public Chat(Contact recipient) { this.recipient = recipient; }
this.recipient = recipient;
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
stream.defaultReadObject();
messages = FXCollections.observableList((List<Message>) stream.readObject());
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeObject(new ArrayList<>(messages));
} }
@Override @Override
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); } public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); }
/** /**
* Generates a hash code based on the recipient. * Generates a hash code based on the recipient.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@Override @Override
@ -58,14 +67,14 @@ public class Chat implements Serializable {
/** /**
* Tests equality to another object based on the recipient. * Tests equality to another object based on the recipient.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj) return true;
if (!(obj instanceof Chat)) return false; if (!(obj instanceof Chat)) return false;
Chat other = (Chat) obj; final Chat other = (Chat) obj;
return Objects.equals(recipient, other.recipient); return Objects.equals(recipient, other.recipient);
} }
@ -76,11 +85,9 @@ public class Chat implements Serializable {
* *
* @param writeProxy the write proxy instance used to notify the server about * @param writeProxy the write proxy instance used to notify the server about
* the message status changes * the message status changes
* @throws IOException if a {@link MessageStatusChange} could not be
* delivered to the server
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void read(WriteProxy writeProxy) throws IOException { public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) { for (int i = messages.size() - 1; i >= 0; --i) {
final Message m = messages.get(i); final Message m = messages.get(i);
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break; if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
@ -101,7 +108,7 @@ public class Chat implements Serializable {
/** /**
* Inserts a message at the correct place according to its creation date. * Inserts a message at the correct place according to its creation date.
* *
* @param message the message to insert * @param message the message to insert
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -116,13 +123,13 @@ public class Chat implements Serializable {
/** /**
* Increments the amount of unread messages. * Increments the amount of unread messages.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void incrementUnreadAmount() { unreadAmount++; } public void incrementUnreadAmount() { unreadAmount++; }
/** /**
* @return the amount of unread mesages in this chat * @return the amount of unread messages in this chat
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public int getUnreadAmount() { return unreadAmount; } public int getUnreadAmount() { return unreadAmount; }
@ -131,7 +138,7 @@ public class Chat implements Serializable {
* @return all messages in the current chat * @return all messages in the current chat
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public List<Message> getMessages() { return messages; } public ObservableList<Message> getMessages() { return messages; }
/** /**
* @return the recipient of a message * @return the recipient of a message
@ -140,14 +147,16 @@ public class Chat implements Serializable {
public Contact getRecipient() { return recipient; } public Contact getRecipient() { return recipient; }
/** /**
* @return whether this {@link Chat} points at a {@link User} * @return the last known time a {@link envoy.event.IsTyping} event has been
* @since Envoy Client v0.1-beta * sent
* @since Envoy Client v0.2-beta
*/ */
public boolean isUserChat() { return recipient instanceof User; } public long getLastWritingEvent() { return lastWritingEvent; }
/** /**
* @return whether this {@link Chat} points at a {@link Group} * Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}.
* @since Envoy Client v0.1-beta *
* @since Envoy Client v0.2-beta
*/ */
public boolean isGroupChat() { return recipient instanceof Group; } public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
} }

View File

@ -2,26 +2,16 @@ package envoy.client.data;
import static java.util.function.Function.identity; import static java.util.function.Function.identity;
import java.io.File;
import java.util.logging.Level;
import envoy.client.ui.Startup;
import envoy.data.Config; import envoy.data.Config;
import envoy.data.ConfigItem;
import envoy.data.LoginCredentials;
/** /**
* Implements a configuration specific to the Envoy Client with default values * Implements a configuration specific to the Envoy Client with default values
* and convenience methods. * and convenience methods.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ClientConfig.java</strong><br>
* Created: <strong>01.03.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class ClientConfig extends Config { public final class ClientConfig extends Config {
private static ClientConfig config; private static ClientConfig config;
@ -35,15 +25,10 @@ public class ClientConfig extends Config {
} }
private ClientConfig() { private ClientConfig() {
items.put("server", new ConfigItem<>("server", "s", identity(), null, true)); super(".envoy");
items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true)); put("server", "s", identity());
items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true)); put("port", "p", Integer::parseInt);
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false)); put("localDBSaveInterval", "db-si", Integer::parseInt);
items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
items.put("user", new ConfigItem<>("user", "u", identity()));
items.put("password", new ConfigItem<>("password", "pw", identity()));
} }
/** /**
@ -59,57 +44,8 @@ public class ClientConfig extends Config {
public Integer getPort() { return (Integer) items.get("port").get(); } public Integer getPort() { return (Integer) items.get("port").get(); }
/** /**
* @return the local database specific to the client user * @return the amount of minutes after which the local database should be saved
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.2-beta
*/ */
public File getLocalDB() { return (File) items.get("localDB").get(); } public Integer getLocalDBSaveInterval() { return (Integer) items.get("localDBSaveInterval").get(); }
/**
* @return {@code true} if the local database is to be ignored
* @since Envoy Client v0.3-alpha
*/
public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
/**
* @return the directory in which all local files are saves
* @since Envoy Client v0.2-alpha
*/
public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
/**
* @return the minimal {@link Level} to log inside the log file
* @since Envoy Client v0.2-alpha
*/
public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
/**
* @return the minimal {@link Level} to log inside the console
* @since Envoy Client v0.2-alpha
*/
public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
/**
* @return the user name
* @since Envoy Client v0.3-alpha
*/
public String getUser() { return (String) items.get("user").get(); }
/**
* @return the password
* @since Envoy Client v0.3-alpha
*/
public String getPassword() { return (String) items.get("password").get(); }
/**
* @return {@code true} if user name and password are set
* @since Envoy Client v0.3-alpha
*/
public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
/**
* @return login credentials for the specified user name and password, without
* the registration option
* @since Envoy Client v0.3-alpha
*/
public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
} }

View File

@ -0,0 +1,93 @@
package envoy.client.data;
import javafx.stage.Stage;
import envoy.client.net.*;
import envoy.client.ui.SceneContext;
/**
* Provides access to commonly used objects.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public class Context {
private WriteProxy writeProxy;
private LocalDB localDB;
private Stage stage;
private SceneContext sceneContext;
private final Client client = new Client();
private static final Context instance = new Context();
private Context() {}
/**
* @return the instance of {@code Context} used throughout Envoy
* @since Envoy Client v0.2-beta
*/
public static Context getInstance() { return instance; }
/**
* Initializes the write proxy given that {@code localDB} is initialized.
*
* @since Envoy Client v0.2-beta
*/
public void initWriteProxy() {
if (localDB == null) throw new IllegalStateException("The LocalDB has to be initialized!");
writeProxy = new WriteProxy(client, localDB);
}
/**
* @return the localDB
* @since Envoy Client v0.2-beta
*/
public LocalDB getLocalDB() { return localDB; }
/**
* @param localDB the localDB to set
* @since Envoy Client v0.2-beta
*/
public void setLocalDB(LocalDB localDB) { this.localDB = localDB; }
/**
* @return the sceneContext
* @since Envoy Client v0.2-beta
*/
public SceneContext getSceneContext() { return sceneContext; }
/**
* @param sceneContext the sceneContext to set. Additionally sets the stage.
* @since Envoy Client v0.2-beta
*/
public void setSceneContext(SceneContext sceneContext) {
this.sceneContext = sceneContext;
stage = sceneContext.getStage();
}
/**
* @return the client
* @since Envoy Client v0.2-beta
*/
public Client getClient() { return client; }
/**
* @return the writeProxy
* @since Envoy Client v0.2-beta
*/
public WriteProxy getWriteProxy() { return writeProxy; }
/**
* @return the stage
* @since Envoy Client v0.2-beta
*/
public Stage getStage() { return stage; }
/**
* @param stage the stage to set
* @since Envoy Client v0.2-beta
*/
public void setStage(Stage stage) { this.stage = stage; }
}

View File

@ -1,27 +1,20 @@
package envoy.client.data; package envoy.client.data;
import java.io.IOException; import java.time.Instant;
import java.time.LocalDateTime;
import envoy.client.net.WriteProxy; import envoy.client.net.WriteProxy;
import envoy.data.Contact; import envoy.data.*;
import envoy.data.GroupMessage;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.data.User;
import envoy.event.GroupMessageStatusChange; import envoy.event.GroupMessageStatusChange;
/** /**
* Represents a chat between a user and a group * Represents a chat between a user and a group
* as a list of messages. * as a list of messages.
* <p> *
* Project: <strong>envoy-client</strong><br>
* File: <strong>GroupChat.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
*
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class GroupChat extends Chat { public final class GroupChat extends Chat {
private final User sender; private final User sender;
@ -38,16 +31,13 @@ public class GroupChat extends Chat {
} }
@Override @Override
public void read(WriteProxy writeProxy) throws IOException { public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) { for (int i = messages.size() - 1; i >= 0; --i) {
final GroupMessage gmsg = (GroupMessage) messages.get(i); final GroupMessage gmsg = (GroupMessage) messages.get(i);
if (gmsg.getSenderID() != sender.getID()) { if (gmsg.getSenderID() != sender.getID()) if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break; else {
else { gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ); writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, Instant.now(), sender.getID()));
writeProxy
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
}
} }
} }
unreadAmount = 0; unreadAmount = 0;

View File

@ -1,89 +1,161 @@
package envoy.client.data; package envoy.client.data;
import java.io.*;
import java.nio.channels.*;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.logging.*;
import javafx.collections.*;
import envoy.client.event.*;
import envoy.data.*; import envoy.data.*;
import envoy.event.GroupResize; import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange; import envoy.event.*;
import envoy.event.NameChange; import envoy.exception.EnvoyException;
import envoy.util.*;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
/** /**
* Stores information about the current {@link User} and their {@link Chat}s. * Stores information about the current {@link User} and their {@link Chat}s.
* For message ID generation a {@link IDGenerator} is stored as well. * For message ID generation a {@link IDGenerator} is stored as well.
* <p> * <p>
* Project: <strong>envoy-client</strong><br> * The managed objects are stored inside a folder in the local file system.
* File: <strong>LocalDB.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public abstract class LocalDB { public final class LocalDB implements EventListener {
protected User user; // Data
protected Map<String, Contact> users = new HashMap<>(); private User user;
protected List<Chat> chats = new ArrayList<>(); private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
protected IDGenerator idGenerator; private ObservableList<Chat> chats = FXCollections.observableArrayList();
protected CacheMap cacheMap = new CacheMap(); private IDGenerator idGenerator;
private CacheMap cacheMap = new CacheMap();
private String authToken;
{ // Auto save timer
private Timer autoSaver;
private boolean autoSaveRestart = true;
// State management
private Instant lastSync = Instant.EPOCH;
// Persistence
private File userFile;
private FileLock instanceLock;
private final File dbDir, idGeneratorFile, lastLoginFile, usersFile;
private static final Logger logger = EnvoyLog.getLogger(LocalDB.class);
/**
* Constructs an empty local database.
*
* @param dbDir the directory in which to persist data
* @throws IOException if {@code dbDir} is a file (and not a directory)
* @throws EnvoyException if {@code dbDir} is in use by another Envoy instance
* @since Envoy Client v0.2-beta
*/
public LocalDB(File dbDir) throws IOException, EnvoyException {
this.dbDir = dbDir;
EventBus.getInstance().registerListener(this);
// Ensure that the database directory exists
if (!dbDir.exists()) dbDir.mkdirs();
else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
// Lock the directory
lock();
// Initialize global files
idGeneratorFile = new File(dbDir, "id_gen.db");
lastLoginFile = new File(dbDir, "last_login.db");
usersFile = new File(dbDir, "users.db");
// Load global files
loadGlobalData();
// Initialize offline caches
cacheMap.put(Message.class, new Cache<>()); cacheMap.put(Message.class, new Cache<>());
cacheMap.put(MessageStatusChange.class, new Cache<>()); cacheMap.put(MessageStatusChange.class, new Cache<>());
} }
/** /**
* Initializes a storage space for a user-specific list of chats. * Ensured that only one Envoy instance is using this local database by creating
* a lock file.
* The lock file is deleted on application exit.
* *
* @since Envoy Client v0.3-alpha * @throws EnvoyException if the lock cannot by acquired
* @since Envoy Client v0.2-beta
*/ */
public void initializeUserStorage() {} private synchronized void lock() throws EnvoyException {
final var file = new File(dbDir, "instance.lock");
try {
final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
instanceLock = fc.tryLock();
if (instanceLock == null) throw new EnvoyException("Another Envoy instance is using this local database!");
} catch (final IOException e) {
throw new EnvoyException("Could not create lock file!", e);
}
}
/** /**
* Stores all users. If the client user is specified, their chats will be stored * Loads the local user registry {@code users.db}, the id generator
* as well. The message id generator will also be saved if present. * {@code id_gen.db} and last login file {@code last_login.db}.
* *
* @throws Exception if the saving process failed * @since Envoy Client v0.2-beta
* @since Envoy Client v0.3-alpha
*/ */
public void save() throws Exception {} private synchronized void loadGlobalData() {
try {
/** try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
* Loads all user data. users = (Map<String, User>) in.readObject();
* }
* @throws Exception if the loading process failed idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
* @since Envoy Client v0.3-alpha try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
*/ user = (User) in.readObject();
public void loadUsers() throws Exception {} authToken = (String) in.readObject();
}
} catch (IOException | ClassNotFoundException e) {}
}
/** /**
* Loads all data of the client user. * Loads all data of the client user.
* *
* @throws Exception if the loading process failed * @throws ClassNotFoundException if the loading process failed
* @throws IOException if the loading process failed
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void loadUserData() throws Exception {} public synchronized void loadUserData() throws ClassNotFoundException, IOException {
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
/** userFile = new File(dbDir, user.getID() + ".db");
* Loads the ID generator. Any exception thrown during this process is ignored. try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
* chats = FXCollections.observableList((List<Chat>) in.readObject());
* @since Envoy Client v0.3-alpha cacheMap = (CacheMap) in.readObject();
*/ lastSync = (Instant) in.readObject();
public void loadIDGenerator() {} } finally {
synchronize();
}
}
/** /**
* Synchronizes the contact list of the client user with the chat and user * Synchronizes the contact list of the client user with the chat and user
* storage. * storage.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void synchronize() { private void synchronize() {
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u)); user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
users.put(user.getName(), user); users.put(user.getName(), user);
// Synchronize user status data // Synchronize user status data
for (Contact contact : users.values()) for (final var contact : users.values())
if (contact instanceof User) if (contact instanceof User) getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(contact.getStatus()); });
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
// Create missing chats // Create missing chats
user.getContacts() user.getContacts()
@ -93,24 +165,147 @@ public abstract class LocalDB {
.forEach(chats::add); .forEach(chats::add);
} }
/**
* Initializes a timer that automatically saves this local database after a
* period of time specified in the settings.
*
* @since Envoy Client v0.2-beta
*/
public void initAutoSave() {
// A logout happened so the timer should be restarted
if (autoSaveRestart) {
autoSaver = new Timer("LocalDB Autosave", true);
autoSaveRestart = false;
}
autoSaver.schedule(new TimerTask() {
@Override
public void run() { save(); }
}, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000);
}
/**
* Stores all users. If the client user is specified, their chats will be stored
* as well. The message id generator will also be saved if present.
*
* @throws IOException if the saving process failed
* @since Envoy Client v0.3-alpha
*/
@Event(eventType = EnvoyCloseEvent.class, priority = 1000)
private synchronized void save() {
EnvoyLog.getLogger(LocalDB.class).log(Level.INFO, "Saving local database...");
// Save users
try {
SerializationUtils.write(usersFile, users);
// Save user data and last sync time stamp
if (user != null) SerializationUtils
.write(userFile, new ArrayList<>(chats), cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
// Save last login information
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
// Save ID generator
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
} catch (final IOException e) {
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", e);
}
}
@Event(priority = 150)
private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) msg.nextStatus(); }
@Event(priority = 150)
private void onGroupMessage(GroupMessage msg) {
// TODO: Cancel event once EventBus is updated
if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ)
logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
}
@Event(priority = 150)
private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); }
@Event(priority = 150)
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
this.<GroupMessage>getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
}
@Event(priority = 150)
private void onUserStatusChange(UserStatusChange evt) {
this.getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get()));
}
@Event(priority = 150)
private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); }
@Event(priority = 150)
private void onNameChange(NameChange evt) {
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny().ifPresent(c -> c.setName(evt.get()));
}
/**
* Stores a new authentication token.
*
* @param evt the event containing the authentication token
* @since Envoy Client v0.2-beta
*/
@Event
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
/**
* Deletes all associations to the current user.
*
* @since Envoy Client v0.2-beta
*/
@Event(eventType = Logout.class, priority = 100)
private void onLogout() {
autoSaver.cancel();
autoSaveRestart = true;
lastLoginFile.delete();
userFile = null;
user = null;
authToken = null;
chats.clear();
lastSync = Instant.EPOCH;
cacheMap.clear();
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their * @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys * user names as keys
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public Map<String, Contact> getUsers() { return users; } public Map<String, User> getUsers() { return users; }
/**
* Searches for a message by ID.
*
* @param id the ID of the message to search for
* @return an optional containing the message
* @since Envoy Client v0.1-beta
*/
public <T extends Message> Optional<T> getMessage(long id) {
return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
}
/**
* Searches for a chat by recipient ID.
*
* @param recipientID the ID of the chat's recipient
* @return an optional containing the chat
* @since Envoy Client v0.1-beta
*/
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
/** /**
* @return all saved {@link Chat} objects that list the client user as the * @return all saved {@link Chat} objects that list the client user as the
* sender * sender
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
**/ **/
public List<Chat> getChats() { return chats; } public ObservableList<Chat> getChats() { return chats; }
/**
* @param chats the chats to set
*/
public void setChats(List<Chat> chats) { this.chats = chats; }
/** /**
* @return the {@link User} who initialized the local database * @return the {@link User} who initialized the local database
@ -134,6 +329,7 @@ public abstract class LocalDB {
* @param idGenerator the message ID generator to set * @param idGenerator the message ID generator to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
@Event(priority = 150)
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; } public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
/** /**
@ -149,57 +345,14 @@ public abstract class LocalDB {
public CacheMap getCacheMap() { return cacheMap; } public CacheMap getCacheMap() { return cacheMap; }
/** /**
* Searches for a message by ID. * @return the time stamp when the database was last saved
* * @since Envoy Client v0.2-beta
* @param id the ID of the message to search for
* @return an optional containing the message
* @since Envoy Client v0.1-beta
*/ */
public Optional<Message> getMessage(long id) { public Instant getLastSync() { return lastSync; }
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
}
/** /**
* Searches for a chat by recipient ID. * @return the authentication token of the user
* * @since Envoy Client v0.2-beta
* @param recipientID the ID of the chat's recipient
* @return an optional containing the chat
* @since Envoy Client v0.1-beta
*/ */
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); } public String getAuthToken() { return authToken; }
/**
* Performs a contact name change if the corresponding contact is present.
*
* @param event the {@link NameChange} to process
* @since Envoy Client v0.1-beta
*/
public void replaceContactName(NameChange event) {
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
}
/**
* Performs a group resize operation if the corresponding group is present.
*
* @param event the {@link GroupResize} to process
* @since Envoy Client v0.1-beta
*/
public void updateGroup(GroupResize event) {
chats.stream()
.map(Chat::getRecipient)
.filter(Group.class::isInstance)
.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
.map(Group.class::cast)
.findAny()
.ifPresent(group -> {
switch (event.getOperation()) {
case ADD:
group.getContacts().add(event.get());
break;
case REMOVE:
group.getContacts().remove(event.get());
break;
}
});
}
} }

View File

@ -1,88 +0,0 @@
package envoy.client.data;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import envoy.data.IDGenerator;
import envoy.util.SerializationUtils;
/**
* Implements a {@link LocalDB} in a way that stores all information inside a
* folder on the local file system.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>PersistentLocalDB.java</strong><br>
* Created: <strong>27.10.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-alpha
*/
public final class PersistentLocalDB extends LocalDB {
private File dbDir, userFile, idGeneratorFile, usersFile;
/**
* Constructs an empty local database. To serialize any user-specific data to
* the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
* and then {@link PersistentLocalDB#save()}.
*
* @param dbDir the directory in which to persist data
* @throws IOException if {@code dbDir} is a file (and not a directory)
* @since Envoy Client v0.1-alpha
*/
public PersistentLocalDB(File dbDir) throws IOException {
this.dbDir = dbDir;
// Test if the database directory is actually a directory
if (dbDir.exists() && !dbDir.isDirectory())
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
// Initialize global files
idGeneratorFile = new File(dbDir, "id_gen.db");
usersFile = new File(dbDir, "users.db");
}
/**
* Creates a database file for a user-specific list of chats.
*
* @throws IllegalStateException if the client user is not specified
* @since Envoy Client v0.1-alpha
*/
@Override
public void initializeUserStorage() {
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
userFile = new File(dbDir, user.getID() + ".db");
}
@Override
public void save() throws IOException {
// Save users
SerializationUtils.write(usersFile, users);
// Save user data
if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
// Save id generator
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
}
@Override
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
@Override
public void loadUserData() throws ClassNotFoundException, IOException {
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
chats = (ArrayList<Chat>) in.readObject();
cacheMap = (CacheMap) in.readObject();
}
}
@Override
public void loadIDGenerator() {
try {
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
} catch (ClassNotFoundException | IOException e) {}
}
}

View File

@ -1,28 +1,27 @@
package envoy.client.data; package envoy.client.data;
import java.io.File; import java.io.*;
import java.io.IOException; import java.util.*;
import java.util.HashMap; import java.util.logging.Level;
import java.util.Map;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import envoy.util.SerializationUtils; import envoy.client.event.EnvoyCloseEvent;
import envoy.util.*;
import dev.kske.eventbus.*;
import dev.kske.eventbus.EventListener;
/** /**
* Manages all application settings, which are different objects that can be * Manages all application settings, which are different objects that can be
* changed during runtime and serialized them by using either the file system or * changed during runtime and serialized them by using either the file system or
* the {@link Preferences} API. * the {@link Preferences} API.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Settings.java</strong><br>
* Created: <strong>11 Nov 2019</strong><br>
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public class Settings { public final class Settings implements EventListener {
// Actual settings accessible by the rest of the application // Actual settings accessible by the rest of the application
private Map<String, SettingsItem<?>> items; private Map<String, SettingsItem<?>> items;
@ -44,6 +43,8 @@ public class Settings {
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
private Settings() { private Settings() {
EventBus.getInstance().registerListener(this);
// Load settings from settings file // Load settings from settings file
try { try {
items = SerializationUtils.read(settingsFile, HashMap.class); items = SerializationUtils.read(settingsFile, HashMap.class);
@ -67,16 +68,28 @@ public class Settings {
* @throws IOException if an error occurs while saving the themes * @throws IOException if an error occurs while saving the themes
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void save() throws IOException { @Event(eventType = EnvoyCloseEvent.class, priority = 900)
private void save() {
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");
// Save settings to settings file // Save settings to settings file
SerializationUtils.write(settingsFile, items); try {
SerializationUtils.write(settingsFile, items);
} catch (final IOException e) {
EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ", e);
}
} }
private void supplementDefaults() { private void supplementDefaults() {
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key.")); items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed.")); items.putIfAbsent("hideOnClose", new SettingsItem<>(false, "Hide on close", "Hides the chat window when it is closed."));
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme.")); items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
items.putIfAbsent("downloadLocation",
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location",
"The location where files will be saved to"));
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?", "Should downloads be saved without asking?"));
items.putIfAbsent("askForConfirmation",
new SettingsItem<>(true, "Ask for confirmation", "Will ask for confirmation before doing certain things"));
} }
/** /**
@ -120,19 +133,71 @@ public class Settings {
*/ */
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); } public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
/**
* @return whether Envoy will prompt a dialogue before saving an
* {@link envoy.data.Attachment}
* @since Envoy Client v0.2-beta
*/
public Boolean isDownloadSavedWithoutAsking() { return (Boolean) items.get("autoSaveDownloads").get(); }
/**
* Sets whether Envoy will prompt a dialogue before saving an
* {@link envoy.data.Attachment}.
*
* @param autosaveDownload whether a download should be saved without asking
* before
* @since Envoy Client v0.2-beta
*/
public void setDownloadSavedWithoutAsking(boolean autosaveDownload) {
((SettingsItem<Boolean>) items.get("autoSaveDownloads")).set(autosaveDownload);
}
/**
* @return the path where downloads should be saved
* @since Envoy Client v0.2-beta
*/
public File getDownloadLocation() { return (File) items.get("downloadLocation").get(); }
/**
* Sets the path where downloads should be saved.
*
* @param downloadLocation the path to set
* @since Envoy Client v0.2-beta
*/
public void setDownloadLocation(File downloadLocation) { ((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation); }
/** /**
* @return the current on close mode. * @return the current on close mode.
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); } public Boolean isHideOnClose() { return (Boolean) items.get("hideOnClose").get(); }
/** /**
* Sets the current on close mode. * Sets the current on close mode.
* *
* @param currentOnCloseMode the on close mode that should be set. * @param hideOnClose whether the application should be minimized on close
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); } public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); }
/**
* @return whether a confirmation dialog should be displayed before certain
* actions
* @since Envoy Client v0.2-alpha
*/
public Boolean isAskForConfirmation() { return (Boolean) items.get("askForConfirmation").get(); }
/**
* Changes the behavior of calling certain functionality by displaying a
* confirmation dialog before executing it.
*
* @param askForConfirmation whether confirmation dialogs should be displayed
* before certain actions
* @since Envoy Client v0.2-alpha
*/
public void setAskForConfirmation(boolean askForConfirmation) {
((SettingsItem<Boolean>) items.get("askForConfirmation")).set(askForConfirmation);
}
/** /**
* @return the items * @return the items

View File

@ -8,16 +8,12 @@ import javax.swing.JComponent;
/** /**
* Encapsulates a persistent value that is directly or indirectly mutable by the * Encapsulates a persistent value that is directly or indirectly mutable by the
* user. * user.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsItem.java</strong><br>
* Created: <strong>23.12.2019</strong><br>
* *
* @param <T> the type of this {@link SettingsItem}'s value * @param <T> the type of this {@link SettingsItem}'s value
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public class SettingsItem<T> implements Serializable { public final class SettingsItem<T> implements Serializable {
private T value; private T value;
private String userFriendlyName, description; private String userFriendlyName, description;

View File

@ -1,15 +0,0 @@
package envoy.client.data;
/**
* Implements a {@link LocalDB} in a way that does not persist any information
* after application shutdown.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>TransientLocalDB.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public final class TransientLocalDB extends LocalDB {
}

View File

@ -6,10 +6,6 @@ import envoy.exception.EnvoyException;
/** /**
* Plays back audio from a byte array. * Plays back audio from a byte array.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>AudioPlayer.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta

View File

@ -1,8 +1,7 @@
package envoy.client.data.audio; package envoy.client.data.audio;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.*;
import java.nio.file.Path;
import javax.sound.sampled.*; import javax.sound.sampled.*;
@ -10,10 +9,6 @@ import envoy.exception.EnvoyException;
/** /**
* Records audio and exports it as a byte array. * Records audio and exports it as a byte array.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>AudioRecorder.java</strong><br>
* Created: <strong>02.07.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -27,6 +22,11 @@ public final class AudioRecorder {
*/ */
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false); public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
/**
* The format in which audio files will be saved.
*/
public static final String FILE_FORMAT = "wav";
private final AudioFormat format; private final AudioFormat format;
private final DataLine.Info info; private final DataLine.Info info;
@ -78,7 +78,7 @@ public final class AudioRecorder {
line.start(); line.start();
// Prepare temp file // Prepare temp file
tempFile = Files.createTempFile("recording", "wav"); tempFile = Files.createTempFile("recording", FILE_FORMAT);
// Start the recording // Start the recording
final var ais = new AudioInputStream(line); final var ais = new AudioInputStream(line);
@ -117,6 +117,6 @@ public final class AudioRecorder {
line.close(); line.close();
try { try {
Files.deleteIfExists(tempFile); Files.deleteIfExists(tempFile);
} catch (IOException e) {} } catch (final IOException e) {}
} }
} }

View File

@ -1,9 +1,5 @@
/** /**
* Contains classes related to recording and playing back audio clips. * Contains classes related to recording and playing back audio clips.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta

View File

@ -0,0 +1,30 @@
package envoy.client.data.commands;
import java.util.function.Supplier;
/**
* This interface defines an action that should be performed when a system
* command gets called.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public interface OnCall {
/**
* Performs class specific actions when a {@link SystemCommand} has been called.
*
* @since Envoy Client v0.2-beta
*/
void onCall();
/**
* Performs actions that can only be performed by classes that are not
* {@link SystemCommand}s when a SystemCommand has been called.
*
* @param consumer the action to perform when this {@link SystemCommand} has
* been called
* @since Envoy Client v0.2-beta
*/
void onCall(Supplier<Void> consumer);
}

View File

@ -0,0 +1,128 @@
package envoy.client.data.commands;
import java.util.*;
import java.util.function.*;
/**
* This class is the base class of all {@code SystemCommands} and contains an
* action and a number of arguments that should be used as input for this
* function.
* No {@code SystemCommand} can return anything.
* Every {@code SystemCommand} must have as argument type {@code List<String>} so
* that the words following the indicator String can be used as input of the
* function. This approach has one limitation:<br>
* <b>Order matters!</b> Changing the order of arguments will likely result in
* unexpected behavior.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class SystemCommand implements OnCall {
protected int relevance;
/**
* The argument count of the command.
*/
protected final int numberOfArguments;
/**
* This function takes a {@code List<String>} as argument because automatically
* {@code SystemCommand#numberOfArguments} words following the necessary command
* will be put into this list.
*
* @see String#split(String)
*/
protected final Consumer<List<String>> action;
protected final String description;
protected final List<String> defaults;
/**
* Constructs a new {@code SystemCommand}.
*
* @param action the action performed by the command
* @param numberOfArguments the argument count accepted by the action
* @param defaults the default values for the corresponding arguments
* @param description the description of this {@code SystemCommand}
* @since Envoy Client v0.2-beta
*/
public SystemCommand(Consumer<List<String>> action, int numberOfArguments, List<String> defaults, String description) {
this.numberOfArguments = numberOfArguments;
this.action = action;
this.defaults = defaults == null ? new ArrayList<>() : defaults;
this.description = description;
}
/**
* @return the action that should be performed
* @since Envoy Client v0.2-beta
*/
public Consumer<List<String>> getAction() { return action; }
/**
* @return the argument count of the command
* @since Envoy Client v0.2-beta
*/
public int getNumberOfArguments() { return numberOfArguments; }
/**
* @return the description
* @since Envoy Client v0.2-beta
*/
public String getDescription() { return description; }
/**
* @return the relevance
* @since Envoy Client v0.2-beta
*/
public int getRelevance() { return relevance; }
/**
* @param relevance the relevance to set
* @since Envoy Client v0.2-beta
*/
public void setRelevance(int relevance) { this.relevance = relevance; }
/**
* Increments the relevance of this {@code SystemCommand}.
*/
@Override
public void onCall() { relevance++; }
/**
* Increments the relevance of this {@code SystemCommand} and executes the
* supplier.
*/
@Override
public void onCall(Supplier<Void> consumer) {
onCall();
consumer.get();
}
/**
* @return the defaults
* @since Envoy Client v0.2-beta
*/
public List<String> getDefaults() { return defaults; }
@Override
public int hashCode() { return Objects.hash(action); }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final SystemCommand other = (SystemCommand) obj;
return Objects.equals(action, other.action);
}
@Override
public String toString() {
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", "
+ (action != null ? "action=" + action + ", " : "") + (description != null ? "description=" + description + ", " : "")
+ (defaults != null ? "defaults=" + defaults : "") + "]";
}
}

View File

@ -0,0 +1,230 @@
package envoy.client.data.commands;
import java.util.*;
import java.util.function.Consumer;
/**
* This class acts as a builder for {@link SystemCommand}s.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class SystemCommandBuilder {
private int numberOfArguments;
private Consumer<List<String>> action;
private List<String> defaults;
private String description;
private int relevance;
private final SystemCommandMap commandsMap;
/**
* Creates a new {@code SystemCommandsBuilder} without underlying
* {@link SystemCommandMap}.
*
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder() { this(null); }
/**
* @param commandsMap the map to use when calling build (optional)
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder(SystemCommandMap commandsMap) { this.commandsMap = commandsMap; }
/**
* @param numberOfArguments the numberOfArguments to set
* @return this {@code SystemCommandBuilder}
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder setNumberOfArguments(int numberOfArguments) {
this.numberOfArguments = numberOfArguments;
return this;
}
/**
* @param action the action to set
* @return this {@code SystemCommandBuilder}
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder setAction(Consumer<List<String>> action) {
this.action = action;
return this;
}
/**
* @param description the description to set
* @return this {@code SystemCommandBuilder}
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder setDescription(String description) {
this.description = description;
return this;
}
/**
* @param relevance the relevance to set
* @return this {@code SystemCommandBuilder}
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder setRelevance(int relevance) {
this.relevance = relevance;
return this;
}
/**
* @param defaults the defaults to set
* @return this {@code SystemCommandBuilder}
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder setDefaults(String... defaults) {
this.defaults = List.of(defaults);
return this;
}
/**
* Resets all values stored.
*
* @return this {@code SystemCommandBuilder}
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder reset() {
numberOfArguments = 0;
action = null;
defaults = new ArrayList<>();
description = "";
relevance = 0;
return this;
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.
*
* @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta
*/
public SystemCommand build() { return build(true); }
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
* previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset.
*
* @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta
*/
public SystemCommand buildNoArg() {
numberOfArguments = 0;
return build(true);
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the
* string as argument, regardless of the previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset.
*
* @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta
*/
public SystemCommand buildRemainingArg() {
numberOfArguments = -1;
return build(true);
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
* not be.
*
* @param reset whether this {@code SystemCommandBuilder} should be reset
* afterwards.<br>
* This can be useful if another command wants to execute something
* similar
* @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta
*/
public SystemCommand build(boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance);
if (reset) reset();
return sc;
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.
* Automatically adds the built object to the given map.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand build(String command) { return build(command, true); }
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
* previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand buildNoArg(String command) {
numberOfArguments = 0;
return build(command, true);
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the
* string as argument, regardless of the previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand buildRemainingArg(String command) {
numberOfArguments = -1;
return build(command, true);
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
* not be.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @param reset whether this {@code SystemCommandBuilder} should be reset
* afterwards.<br>
* This can be useful if another command wants to execute
* something
* similar
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand build(String command, boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance);
if (commandsMap != null) commandsMap.add(command, sc);
else throw new NullPointerException("No map in SystemCommandsBuilder present");
if (reset) reset();
return sc;
}
}

View File

@ -0,0 +1,260 @@
package envoy.client.data.commands;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import envoy.util.EnvoyLog;
/**
* This class stores all {@link SystemCommand}s used.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class SystemCommandMap {
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$");
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
/**
* Adds a new command to the map if the command name is valid.
*
* @param command the input string to execute the
* given action
* @param systemCommand the command to add - can be built using
* {@link SystemCommandBuilder}
* @see SystemCommandMap#isValidKey(String)
* @since Envoy Client v0.2-beta
*/
public void add(String command, SystemCommand systemCommand) {
if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand);
}
/**
* This method checks if the input String is a key in the map and returns the
* wrapped System command if present.
* It will return an empty optional if the value after the slash is not a key in
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
* map).
* <p>
* Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();}
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
* {@code ....}<br>
* user input: {@code "/example xyz ..."}<br>
* {@code systemCommands.get("example xyz ...")} or
* {@code systemCommands.get("/example xyz ...")}
* result: {@code Optional<SystemCommand>}
*
* @param input the input string given by the user
* @return the wrapped system command, if present
* @since Envoy Client v0.2-beta
*/
public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); }
/**
* This method ensures that the "/" of a {@link SystemCommand} is stripped.<br>
* It returns the command as (most likely) entered as key in the map for the
* first word of the text.<br>
* It should only be called on strings that contain a "/" at position 0/-1.
*
* @param raw the input
* @return the command as entered in the map
* @since Envoy Client v0.2-beta
* @apiNote this method will (most likely) not return anything useful if
* whatever is entered after the slash is not a system command. Only
* exception: for recommendation purposes.
*/
public String getCommand(String raw) {
final var trimmed = raw.stripLeading();
final var index = trimmed.indexOf(' ');
return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
}
/**
* Examines whether a key can be put in the map and logs it with
* {@code Level.WARNING} if that key violates API constrictions.<br>
* (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>)
* <p>
* The approach to not throw an exception was taken so that an ugly try-catch
* block for every addition to the system commands map could be avoided, an
* error that should only occur during implementation and not in production.
*
* @param command the key to examine
* @return whether this key can be used in the map
* @since Envoy Client v0.2-beta
*/
public boolean isValidKey(String command) {
final boolean valid = commandPattern.matcher(command).matches();
if (!valid) logger.log(Level.WARNING,
"The command \"" + command
+ "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters "
+ commandPattern + "are allowed");
return valid;
}
/**
* Takes a 'raw' string (the whole input) and checks if "/" is the first visible
* character and then checks if a command is present after that "/". If that is
* the case, it will be executed.
* <p>
*
* @param raw the raw input string
* @return whether a command could be found
* @since Envoy Client v0.2-beta
*/
public boolean executeIfAnyPresent(String raw) {
// possibly a command was detected and could be executed
final var raw2 = raw.stripLeading();
final var commandFound = raw2.startsWith("/") ? executeIfPresent(raw2) : false;
// the command was executed successfully - no further checking needed
if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2));
return commandFound;
}
/**
* This method checks if the input String is a key in the map and executes the
* wrapped System command if present.
* Its intended usage is after a "/" has been detected in the input String.
* It will do nothing if the value after the slash is not a key in
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
* map).
* <p>
* Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
* {@code ....}<br>
* user input: {@code "/example xyz ..."}<br>
* {@code systemCommands.executeIfPresent("example xyz ...")}
* result: {@code button.getText()=="xyz"}
*
* @param input the input string given by the user
* @return whether a command could be found
* @since Envoy Client v0.2-beta
*/
public boolean executeIfPresent(String input) {
final var command = getCommand(input);
final var value = get(command);
value.ifPresent(systemCommand -> {
// Splitting the String so that the leading command including the first " " is
// removed and only as many following words as allowed by the system command
// persist
final var arguments = extractArguments(input, systemCommand);
// Executing the function
try {
systemCommand.getAction().accept(arguments);
systemCommand.onCall();
} catch (final Exception e) {
logger.log(Level.WARNING, "The system command " + command + " threw an exception: ", e);
}
});
return value.isPresent();
}
/**
* Supplies missing values with default values.
*
* @param input the input String
* @param systemCommand the command that is expected
* @return the list of arguments that can be used to parse the systemCommand
* @since Envoy Client v0.2-beta
*/
private List<String> extractArguments(String input, SystemCommand systemCommand) {
// no more arguments follow after the command (e.g. text = "/DABR")
final var indexOfSpace = input.indexOf(" ");
if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand);
// the arguments behind a system command
final var remainingString = input.substring(indexOfSpace + 1);
final var numberOfArguments = systemCommand.getNumberOfArguments();
// splitting those arguments and supplying default values
final var textArguments = remainingString.split(" ", -1);
final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments;
final var arguments = supplementDefaults(originalArguments, systemCommand);
return arguments;
}
/**
* Retrieves the recommendations based on the current input entered.<br>
* The first word is used for the recommendations and
* it does not matter if the "/" is at its beginning or not.<br>
* If none are present, nothing will be done.<br>
* Otherwise the given function will be executed on the recommendations.<br>
*
* @param input the input string
* @param action the action that should be taken for the recommendations, if any
* are present
* @since Envoy Client v0.2-beta
*/
public void requestRecommendations(String input, Consumer<Set<String>> action) {
final var partialCommand = getCommand(input);
// Get the expected commands
final var recommendations = recommendCommands(partialCommand);
if (recommendations.isEmpty()) return;
// Execute the given action
else action.accept(recommendations);
}
/**
* Recommends commands based upon the currently entered input.<br>
* In the current implementation, all we check is whether a key contains this
* input. This might be updated later on.
*
* @param partialCommand the partially entered command
* @return a set of all commands that match this input
* @since Envoy Client v0.2-beta
*/
private Set<String> recommendCommands(String partialCommand) {
// current implementation only looks if input is contained within a command,
// might be updated
return systemCommands.keySet()
.stream()
.filter(command -> command.contains(partialCommand))
.sorted((command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(), systemCommands.get(command2).getRelevance()))
.collect(Collectors.toSet());
}
/**
*
* Supplies the default values for arguments if none are present in the text for
* any argument. <br>
* Will only work for {@code SystemCommand}s whose argument counter is bigger
* than 1.
*
* @param textArguments the arguments that were parsed from the text
* @param toEvaluate the system command whose default values should be used
* @return the final argument list
* @since Envoy Client v0.2-beta
* @apiNote this method will insert an empty String if the size of the list
* given to the {@code SystemCommand} is smaller than its argument
* counter and no more text arguments could be found.
*/
private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
final var defaults = toEvaluate.getDefaults();
final var numberOfArguments = toEvaluate.getNumberOfArguments();
final List<String> result = new ArrayList<>();
if (toEvaluate.getNumberOfArguments() > 0) for (int index = 0; index < numberOfArguments; index++) {
String textArg = null;
if (index < textArguments.length) textArg = textArguments[index];
// Set the argument at position index to the current argument of the text, if it
// is present. Otherwise the default for that argument will be taken if present.
// In the worst case, an empty String will be used.
result.add(!(textArg == null) && !textArg.isBlank() ? textArg : index < defaults.size() ? defaults.get(index) : "");
}
return result;
}
/**
* @return all {@link SystemCommand}s used with the underlying command as key
* @since Envoy Client v0.2-beta
*/
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; }
}

View File

@ -0,0 +1,8 @@
/**
* This package contains all classes that can be used as system commands.<br>
* Every system command can be called using a specific syntax:"/&lt;command&gt;"
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
package envoy.client.data.commands;

View File

@ -0,0 +1,15 @@
package envoy.client.event;
import envoy.event.Event.Valueless;
/**
* This event serves the purpose of triggering the tab change to tab 0 in
* {@link envoy.client.ui.controller.ChatScene}.
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.2-beta
*/
public class BackEvent extends Valueless {
private static final long serialVersionUID = 0L;
}

View File

@ -0,0 +1,16 @@
package envoy.client.event;
import envoy.event.Event.Valueless;
/**
* This event notifies various Envoy components of the application being about
* to shut down. This allows the graceful closing of connections, persisting
* local data etc.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public class EnvoyCloseEvent extends Valueless {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,14 @@
package envoy.client.event;
import envoy.event.Event.Valueless;
/**
* Indicates that a logout has been requested.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class Logout extends Valueless {
private static final long serialVersionUID = 1L;
}

View File

@ -1,22 +0,0 @@
package envoy.client.event;
import envoy.data.Message;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageCreationEvent.java</strong><br>
* Created: <strong>4 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class MessageCreationEvent extends Event<Message> {
private static final long serialVersionUID = 0L;
/**
* @param message the {@link Message} that has been created
*/
public MessageCreationEvent(Message message) { super(message); }
}

View File

@ -1,22 +0,0 @@
package envoy.client.event;
import envoy.data.Message;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageModificationEvent.java</strong><br>
* Created: <strong>4 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class MessageModificationEvent extends Event<Message> {
private static final long serialVersionUID = 0L;
/**
* @param message the {@link Message} that has been modified
*/
public MessageModificationEvent(Message message) { super(message); }
}

View File

@ -1,22 +0,0 @@
package envoy.client.event;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SendEvent.java</strong><br>
* Created: <strong>11.02.2020</strong><br>
*
* @author: Maximilian K&aumlfer
* @since Envoy Client v0.3-alpha
*/
public class SendEvent extends Event<Event<?>> {
private static final long serialVersionUID = 0L;
/**
* @param value the event to send to the server
*/
public SendEvent(Event<?> value) { super(value); }
}

View File

@ -3,23 +3,12 @@ package envoy.client.event;
import envoy.event.Event; import envoy.event.Event;
/** /**
* Project: <strong>envoy-client</strong><br> * Notifies UI components of a theme change.
* File: <strong>ThemeChangeEvent.java</strong><br>
* Created: <strong>15 Dec 2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public class ThemeChangeEvent extends Event<String> { public final class ThemeChangeEvent extends Event.Valueless {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
/**
* Initializes a {@link ThemeChangeEvent} conveying information about the change
* of the theme currently in use.
*
* @param theme the name of the new theme
* @since Envoy Client v0.2-alpha
*/
public ThemeChangeEvent(String theme) { super(theme); }
} }

View File

@ -0,0 +1,36 @@
package envoy.client.helper;
import javafx.scene.control.*;
import envoy.client.data.Settings;
/**
* Provides methods that are commonly used for alerts.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class AlertHelper {
private AlertHelper() {}
/**
* Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()}
* returns {@code true}.
* Immediately executes the action if no dialog was requested or the dialog was
* exited with a confirmation.
* Does nothing if the dialog was closed without clicking on OK.
*
* @param alert the (customized) alert to show. <strong>Should not be shown
* already</strong>
* @param action the action to perform in case of success
* @since Envoy Client v0.2-beta
*/
public static void confirmAction(Alert alert, Runnable action) {
alert.setHeight(225);
alert.setWidth(400);
alert.setHeaderText("");
if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
else action.run();
}
}

View File

@ -0,0 +1,57 @@
package envoy.client.helper;
import java.util.logging.Level;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*;
import envoy.client.event.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus;
/**
* Simplifies shutdown actions.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class ShutdownHelper {
private ShutdownHelper() {}
/**
* Exits Envoy or minimizes it, depending on the current state of
* {@link Settings#isHideOnClose()}.
*
* @since Envoy Client v0.2-beta
*/
public static void exit() {
if (Settings.getInstance().isHideOnClose()) Context.getInstance().getStage().setIconified(true);
else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0);
}
}
/**
* Logs the current user out and reopens
* {@link envoy.client.ui.controller.LoginScene}.
*
* @since Envoy Client v0.2-beta
*/
public static void logout() {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Logout?");
alert.setContentText("Are you sure you want to log out?");
AlertHelper.confirmAction(alert, () -> {
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
EventBus.getInstance().dispatch(new Logout());
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
});
}
}

View File

@ -0,0 +1,9 @@
/**
* Provides helper methods that reduce boilerplate code.
*
* @author Leon Hofmeister
* @author Kai S. K. Engelbert
* @author Maximilian K&auml;fer
* @since Envoy Client v0.2-beta
*/
package envoy.client.helper;

View File

@ -1,35 +1,29 @@
package envoy.client.net; package envoy.client.net;
import java.io.Closeable; import java.io.*;
import java.io.IOException;
import java.net.Socket; import java.net.Socket;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.SendEvent; import envoy.client.event.EnvoyCloseEvent;
import envoy.data.*; import envoy.data.*;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.ContactOperation; import envoy.util.*;
import envoy.event.contact.ContactSearchResult;
import envoy.util.EnvoyLog; import dev.kske.eventbus.*;
import envoy.util.SerializationUtils; import dev.kske.eventbus.Event;
/** /**
* Establishes a connection to the server, performs a handshake and delivers * Establishes a connection to the server, performs a handshake and delivers
* certain objects to the server. * certain objects to the server.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Client.java</strong><br>
* Created: <strong>28 Sep 2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
*/ */
public class Client implements Closeable { public final class Client implements EventListener, Closeable {
// Connection handling // Connection handling
private Socket socket; private Socket socket;
@ -45,6 +39,13 @@ public class Client implements Closeable {
private static final Logger logger = EnvoyLog.getLogger(Client.class); private static final Logger logger = EnvoyLog.getLogger(Client.class);
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
/**
* Constructs a client and registers it as an event listener.
*
* @since Envoy Client v0.2-beta
*/
public Client() { eventBus.registerListener(this); }
/** /**
* Enters the online mode by acquiring a user ID from the server. As a * Enters the online mode by acquiring a user ID from the server. As a
* connection has to be established and a handshake has to be made, this method * connection has to be established and a handshake has to be made, this method
@ -69,10 +70,10 @@ public class Client implements Closeable {
// Create object receiver // Create object receiver
receiver = new Receiver(socket.getInputStream()); receiver = new Receiver(socket.getInputStream());
// Register user creation processor, contact list processor and message cache // Register user creation processor, contact list processor, message cache and
// authentication token
receiver.registerProcessor(User.class, sender -> this.sender = sender); receiver.registerProcessor(User.class, sender -> this.sender = sender);
receiver.registerProcessors(cacheMap.getMap()); receiver.registerProcessors(cacheMap.getMap());
receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
rejected = false; rejected = false;
@ -99,7 +100,6 @@ public class Client implements Closeable {
} }
online = true; online = true;
logger.log(Level.INFO, "Handshake completed."); logger.log(Level.INFO, "Handshake completed.");
} }
@ -120,99 +120,86 @@ public class Client implements Closeable {
// Remove all processors as they are only used during the handshake // Remove all processors as they are only used during the handshake
receiver.removeAllProcessors(); receiver.removeAllProcessors();
// Process incoming messages
final var receivedMessageProcessor = new ReceivedMessageProcessor();
final var receivedGroupMessageProcessor = new ReceivedGroupMessageProcessor();
final var messageStatusChangeProcessor = new MessageStatusChangeProcessor();
final var groupMessageStatusChangeProcessor = new GroupMessageStatusChangeProcessor();
receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor);
receiver.registerProcessor(Message.class, receivedMessageProcessor);
receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor);
receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor);
// Relay cached messages and message status changes // Relay cached messages and message status changes
cacheMap.get(Message.class).setProcessor(receivedMessageProcessor); cacheMap.get(Message.class).setProcessor(eventBus::dispatch);
cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor); cacheMap.get(GroupMessage.class).setProcessor(eventBus::dispatch);
cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor); cacheMap.get(MessageStatusChange.class).setProcessor(eventBus::dispatch);
cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor); cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch);
// Process user status changes
receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch);
// Process message ID generation
receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
// Process name changes
receiver.registerProcessor(NameChange.class, evt -> { localDB.replaceContactName(evt); eventBus.dispatch(evt); });
// Process contact searches
receiver.registerProcessor(ContactSearchResult.class, eventBus::dispatch);
// Process contact operations
receiver.registerProcessor(ContactOperation.class, eventBus::dispatch);
// Process group size changes
receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
// Send event
eventBus.register(SendEvent.class, evt -> {
try {
sendEvent(evt.get());
} catch (final IOException e) {
logger.log(Level.WARNING, "An error occurred when trying to send " + evt, e);
}
});
// Request a generator if none is present or the existing one is consumed // Request a generator if none is present or the existing one is consumed
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIdGenerator(); if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIDGenerator();
// Relay caches // Relay caches
cacheMap.getMap().values().forEach(Cache::relay); cacheMap.getMap().values().forEach(Cache::relay);
} }
/**
* Sends an object to the server.
*
* @param obj the object to send
* @throws IllegalStateException if the client is not online
* @throws RuntimeException if the object serialization failed
* @since Envoy Client v0.2-beta
*/
public void send(Serializable obj) throws IllegalStateException, RuntimeException {
checkOnline();
logger.log(Level.FINE, "Sending " + obj);
try {
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** /**
* Sends a message to the server. The message's status will be incremented once * Sends a message to the server. The message's status will be incremented once
* it was delivered successfully. * it was delivered successfully.
* *
* @param message the message to send * @param message the message to send
* @throws IOException if the message does not reach the server
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void sendMessage(Message message) throws IOException { public void sendMessage(Message message) {
writeObject(message); send(message);
message.nextStatus(); message.nextStatus();
} }
/**
* Sends an event to the server.
*
* @param evt the event to send
* @throws IOException if the event did not reach the server
*/
public void sendEvent(Event<?> evt) throws IOException { writeObject(evt); }
/** /**
* Requests a new {@link IDGenerator} from the server. * Requests a new {@link IDGenerator} from the server.
* *
* @throws IOException if the request does not reach the server
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void requestIdGenerator() throws IOException { public void requestIDGenerator() {
logger.log(Level.INFO, "Requesting new id generator..."); logger.log(Level.INFO, "Requesting new id generator...");
writeObject(new IDGeneratorRequest()); send(new IDGeneratorRequest());
} }
@Event(eventType = HandshakeRejection.class, priority = 1000)
private void onHandshakeRejection() { rejected = true; }
@Override @Override
public void close() throws IOException { if (online) socket.close(); } @Event(eventType = EnvoyCloseEvent.class, priority = 800)
public void close() {
if (online) {
logger.log(Level.INFO, "Closing connection...");
try {
private void writeObject(Object obj) throws IOException { // The sender must be reset as otherwise the handshake is immediately closed
checkOnline(); sender = null;
logger.log(Level.FINE, "Sending " + obj); online = false;
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream()); socket.close();
} catch (final IOException e) {
logger.log(Level.WARNING, "Failed to close socket: ", e);
}
}
} }
private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); } /**
* Ensured that the client is online.
*
* @throws IllegalStateException if the client is not online
* @since Envoy Client v0.3-alpha
*/
private void checkOnline() throws IllegalStateException { if (!online) throw new IllegalStateException("Client is not online"); }
/** /**
* @return the {@link User} as which this client is logged in * @return the {@link User} as which this client is logged in
@ -230,6 +217,7 @@ public class Client implements Closeable {
/** /**
* @return the {@link Receiver} used by this {@link Client} * @return the {@link Receiver} used by this {@link Client}
* @since v0.2-alpha
*/ */
public Receiver getReceiver() { return receiver; } public Receiver getReceiver() { return receiver; }

View File

@ -1,29 +0,0 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.event.GroupMessageStatusChange;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>GroupMessageStatusChangePocessor.java</strong><br>
* Created: <strong>03.07.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupMessageStatusChangeProcessor implements Consumer<GroupMessageStatusChange> {
private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class);
@Override
public void accept(GroupMessageStatusChange evt) {
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt);
else EventBus.getInstance().dispatch(evt);
}
}

View File

@ -1,35 +0,0 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageStatusChangeProcessor.java</strong><br>
* Created: <strong>4 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class MessageStatusChangeProcessor implements Consumer<MessageStatusChange> {
private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
/**
* Dispatches a {@link MessageStatusChange} if the status is
* {@code RECEIVED} or {@code READ}.
*
* @param evt the status change event
* @since Envoy Client v0.3-alpha
*/
@Override
public void accept(MessageStatusChange evt) {
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid message status change " + evt);
else EventBus.getInstance().dispatch(evt);
}
}

View File

@ -1,33 +0,0 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.client.event.MessageCreationEvent;
import envoy.data.GroupMessage;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ReceivedGroupMessageProcessor.java</strong><br>
* Created: <strong>13.06.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class ReceivedGroupMessageProcessor implements Consumer<GroupMessage> {
private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class);
@Override
public void accept(GroupMessage groupMessage) {
if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ)
logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus());
// Dispatch event
EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage));
}
}

View File

@ -1,36 +0,0 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.client.event.MessageCreationEvent;
import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ReceivedMessageProcessor.java</strong><br>
* Created: <strong>31.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class ReceivedMessageProcessor implements Consumer<Message> {
private static final Logger logger = EnvoyLog.getLogger(ReceivedMessageProcessor.class);
@Override
public void accept(Message message) {
if (message.getStatus() != MessageStatus.SENT) logger.log(Level.WARNING, "The message has the unexpected status " + message.getStatus());
else {
// Update status to RECEIVED
message.nextStatus();
// Dispatch event
EventBus.getInstance().dispatch(new MessageCreationEvent(message));
}
}
}

View File

@ -1,35 +1,31 @@
package envoy.client.net; package envoy.client.net;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.SocketException; import java.net.SocketException;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import envoy.util.EnvoyLog; import envoy.util.*;
import envoy.util.SerializationUtils;
import dev.kske.eventbus.*;
/** /**
* Receives objects from the server and passes them to processor objects based * Receives objects from the server and passes them to processor objects based
* on their class. * on their class.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Receiver.java</strong><br>
* Created: <strong>30.12.2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public class Receiver extends Thread { public final class Receiver extends Thread {
private boolean isAlive = true;
private final InputStream in; private final InputStream in;
private final Map<Class<?>, Consumer<?>> processors = new HashMap<>(); private final Map<Class<?>, Consumer<?>> processors = new HashMap<>();
private static final Logger logger = EnvoyLog.getLogger(Receiver.class); private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
/** /**
* Creates an instance of {@link Receiver}. * Creates an instance of {@link Receiver}.
@ -40,6 +36,7 @@ public class Receiver extends Thread {
public Receiver(InputStream in) { public Receiver(InputStream in) {
super("Receiver"); super("Receiver");
this.in = in; this.in = in;
setDaemon(true);
} }
/** /**
@ -51,7 +48,7 @@ public class Receiver extends Thread {
@Override @Override
public void run() { public void run() {
while (true) { while (isAlive)
try { try {
// Read object length // Read object length
final byte[] lenBytes = new byte[4]; final byte[] lenBytes = new byte[4];
@ -66,6 +63,12 @@ public class Receiver extends Thread {
// Catch LV encoding errors // Catch LV encoding errors
if (len != bytesRead) { if (len != bytesRead) {
// Server has stopped sending, i.e. because he went offline
if (bytesRead == -1) {
isAlive = false;
logger.log(Level.INFO, "Lost connection to the server. Exiting receiver...");
continue;
}
logger.log(Level.WARNING, logger.log(Level.WARNING,
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead)); String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
continue; continue;
@ -78,17 +81,22 @@ public class Receiver extends Thread {
// Get appropriate processor // Get appropriate processor
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
final Consumer processor = processors.get(obj.getClass()); final Consumer processor = processors.get(obj.getClass());
if (processor == null)
logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass())); // Dispatch to the processor if present
else processor.accept(obj); if (processor != null) processor.accept(obj);
// Dispatch to the event bus if the object is an event without a processor
else if (obj instanceof IEvent) eventBus.dispatch((IEvent) obj);
// Notify if no processor could be located
else logger.log(Level.WARNING,
String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
} }
} catch (final SocketException e) { } catch (final SocketException | EOFException e) {
// Connection probably closed by client. // Connection probably closed by client.
logger.log(Level.INFO, "Exiting receiver...");
return; return;
} catch (final Exception e) { } catch (final Exception e) {
logger.log(Level.SEVERE, "Error on receiver thread", e); logger.log(Level.SEVERE, "Error on receiver thread", e);
} }
}
} }
/** /**
@ -103,7 +111,7 @@ public class Receiver extends Thread {
/** /**
* Adds a map of object processors to this {@link Receiver}. * Adds a map of object processors to this {@link Receiver}.
* *
* @param processors the processors to add the processors to add * @param processors the processors to add the processors to add
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */

View File

@ -1,11 +1,8 @@
package envoy.client.net; package envoy.client.net;
import java.io.IOException; import java.util.logging.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.client.data.Cache; import envoy.client.data.*;
import envoy.client.data.LocalDB;
import envoy.data.Message; import envoy.data.Message;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
@ -14,15 +11,11 @@ import envoy.util.EnvoyLog;
* Implements methods to send {@link Message}s and * Implements methods to send {@link Message}s and
* {@link MessageStatusChange}s to the server or cache them inside a * {@link MessageStatusChange}s to the server or cache them inside a
* {@link LocalDB} depending on the online status. * {@link LocalDB} depending on the online status.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>WriteProxy.java</strong><br>
* Created: <strong>6 Feb 2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public class WriteProxy { public final class WriteProxy {
private final Client client; private final Client client;
private final LocalDB localDB; private final LocalDB localDB;
@ -33,10 +26,9 @@ public class WriteProxy {
* Initializes a write proxy using a client and a local database. The * Initializes a write proxy using a client and a local database. The
* corresponding cache processors are injected into the caches. * corresponding cache processors are injected into the caches.
* *
* @param client the client used to send messages and message status change * @param client the client instance used to send messages and events if online
* events * @param localDB the local database used to cache messages and events if
* @param localDB the local database used to cache messages and message status * offline
* change events
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public WriteProxy(Client client, LocalDB localDB) { public WriteProxy(Client client, LocalDB localDB) {
@ -45,20 +37,12 @@ public class WriteProxy {
// Initialize cache processors for messages and message status change events // Initialize cache processors for messages and message status change events
localDB.getCacheMap().get(Message.class).setProcessor(msg -> { localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
try { logger.log(Level.FINER, "Sending cached " + msg);
logger.log(Level.FINER, "Sending cached " + msg); client.sendMessage(msg);
client.sendMessage(msg);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Could not send cached message: ", e);
}
}); });
localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> { localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
logger.log(Level.FINER, "Sending cached " + evt); logger.log(Level.FINER, "Sending cached " + evt);
try { client.send(evt);
client.sendEvent(evt);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
}
}); });
} }
@ -68,19 +52,16 @@ public class WriteProxy {
* *
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void flushCache() { public void flushCache() { localDB.getCacheMap().getMap().values().forEach(Cache::relay); }
localDB.getCacheMap().getMap().values().forEach(Cache::relay);
}
/** /**
* Delivers a message to the server if online. Otherwise the message is cached * Delivers a message to the server if online. Otherwise the message is cached
* inside the local database. * inside the local database.
* *
* @param message the message to send * @param message the message to send
* @throws IOException if the message could not be sent
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void writeMessage(Message message) throws IOException { public void writeMessage(Message message) {
if (client.isOnline()) client.sendMessage(message); if (client.isOnline()) client.sendMessage(message);
else localDB.getCacheMap().getApplicable(Message.class).accept(message); else localDB.getCacheMap().getApplicable(Message.class).accept(message);
} }
@ -90,11 +71,10 @@ public class WriteProxy {
* event is cached inside the local database. * event is cached inside the local database.
* *
* @param evt the event to send * @param evt the event to send
* @throws IOException if the event could not be sent
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void writeMessageStatusChange(MessageStatusChange evt) throws IOException { public void writeMessageStatusChange(MessageStatusChange evt) {
if (client.isOnline()) client.sendEvent(evt); if (client.isOnline()) client.send(evt);
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt); else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
} }
} }

View File

@ -1,169 +0,0 @@
package envoy.client.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
/**
* This class offers a text field that is automatically equipped with a clear
* button.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ClearableTextField.java</strong><br>
* Created: <strong>25.06.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class ClearableTextField extends GridPane {
private final TextField textField;
private final Button clearButton;
/**
* Constructs a new {@code ClearableTextField} with no initial text and icon
* size 16.
*
* @since Envoy Client v0.1-beta
*/
public ClearableTextField() { this("", 16); }
/**
* Constructs a new {@code ClearableTextField} with initial text and a
* predetermined icon size.
*
* @param text the text that should be displayed by default
* @param size the size of the icon
* @since Envoy Client v0.1-beta
*/
public ClearableTextField(String text, int size) {
// initializing the textField and the button
textField = new TextField(text);
clearButton = new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
clearButton.setOnAction(e -> textField.clear());
clearButton.setFocusTraversable(false);
clearButton.getStyleClass().clear();
clearButton.setBackground(Background.EMPTY);
// Adding the two elements to the GridPane
add(textField, 0, 0, 2, 1);
add(clearButton, 1, 0, 1, 1);
// Setting the percent - widths of the two columns.
// Used to locate the button on the right.
final var columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(90);
getColumnConstraints().add(columnConstraints);
final var columnConstraints2 = new ColumnConstraints();
columnConstraints2.setPercentWidth(10);
getColumnConstraints().add(columnConstraints2);
}
/**
* @return the underlying {@code textField}
* @since Envoy Client v0.1-beta
*/
public TextField getTextField() { return textField; }
/**
* This method offers the freedom to perform custom actions when the
* {@code clearButton} has been pressed.
* <p>
* The default is
* <b><code> e -> {clearableTextField.getTextField().clear();}</code></b>
*
* @param onClearButtonAction the action that should be performed
* @since Envoy Client v0.1-beta
*/
public void setClearButtonListener(EventHandler<ActionEvent> onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); }
/**
* @return the current property of the prompt text
* @see javafx.scene.control.TextInputControl#promptTextProperty()
* @since Envoy Client v0.1-beta
*/
public final StringProperty promptTextProperty() { return textField.promptTextProperty(); }
/**
* @return the current prompt text
* @see javafx.scene.control.TextInputControl#getPromptText()
* @since Envoy Client v0.1-beta
*/
public final String getPromptText() { return textField.getPromptText(); }
/**
* @param value the prompt text to display
* @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String)
* @since Envoy Client v0.1-beta
*/
public final void setPromptText(String value) { textField.setPromptText(value); }
/**
* @return the current property of the tooltip
* @see javafx.scene.control.Control#tooltipProperty()
* @since Envoy Client v0.1-beta
*/
public final ObjectProperty<Tooltip> tooltipProperty() { return textField.tooltipProperty(); }
/**
* @param value the new tooltip
* @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip)
* @since Envoy Client v0.1-beta
*/
public final void setTooltip(Tooltip value) { textField.setTooltip(value); }
/**
* @return the current tooltip
* @see javafx.scene.control.Control#getTooltip()
* @since Envoy Client v0.1-beta
*/
public final Tooltip getTooltip() { return textField.getTooltip(); }
/**
* @return the current property of the context menu
* @see javafx.scene.control.Control#contextMenuProperty()
* @since Envoy Client v0.1-beta
*/
public final ObjectProperty<ContextMenu> contextMenuProperty() { return textField.contextMenuProperty(); }
/**
* @param value the new context menu
* @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu)
* @since Envoy Client v0.1-beta
*/
public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); }
/**
* @return the current context menu
* @see javafx.scene.control.Control#getContextMenu()
* @since Envoy Client v0.1-beta
*/
public final ContextMenu getContextMenu() { return textField.getContextMenu(); }
/**
* @param value whether this ClearableTextField should be editable
* @see javafx.scene.control.TextInputControl#setEditable(boolean)
* @since Envoy Client v0.1-beta
*/
public final void setEditable(boolean value) { textField.setEditable(value); }
/**
* @return the current property whether this ClearableTextField is editable
* @see javafx.scene.control.TextInputControl#editableProperty()
* @since Envoy Client v0.1-beta
*/
public final BooleanProperty editableProperty() { return textField.editableProperty(); }
/**
* @return whether this {@code ClearableTextField} is editable
* @see javafx.scene.control.TextInputControl#isEditable()
* @since Envoy Client v0.1-beta
*/
public final boolean isEditable() { return textField.isEditable(); }
}

View File

@ -3,10 +3,6 @@ package envoy.client.ui;
/** /**
* This interface defines an action that should be performed when a scene gets * This interface defines an action that should be performed when a scene gets
* restored from the scene stack in {@link SceneContext}. * restored from the scene stack in {@link SceneContext}.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Restorable.java</strong><br>
* Created: <strong>03.07.2020</strong><br>
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta

View File

@ -4,16 +4,19 @@ import java.io.IOException;
import java.util.Stack; import java.util.Stack;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Parent; import javafx.scene.*;
import javafx.scene.Scene; import javafx.scene.input.*;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.data.Settings; import envoy.client.data.Settings;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.*;
import envoy.event.EventBus; import envoy.client.helper.ShutdownHelper;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
/** /**
* Manages a stack of scenes. The most recently added scene is displayed inside * Manages a stack of scenes. The most recently added scene is displayed inside
* a stage. When a scene is removed from the stack, its predecessor is * a stage. When a scene is removed from the stack, its predecessor is
@ -21,15 +24,11 @@ import envoy.util.EnvoyLog;
* <p> * <p>
* When a scene is loaded, the style sheet for the current theme is applied to * When a scene is loaded, the style sheet for the current theme is applied to
* it. * it.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SceneContext.java</strong><br>
* Created: <strong>06.06.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class SceneContext { public final class SceneContext implements EventListener {
/** /**
* Contains information about different scenes and their FXML resource files. * Contains information about different scenes and their FXML resource files.
@ -53,33 +52,12 @@ public final class SceneContext {
*/ */
SETTINGS_SCENE("/fxml/SettingsScene.fxml"), SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the contact search screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
/**
* The scene in which the group creation screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
GROUP_CREATION_SCENE("/fxml/GroupCreationScene.fxml"),
/** /**
* The scene in which the login screen is displayed. * The scene in which the login screen is displayed.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
LOGIN_SCENE("/fxml/LoginScene.fxml"), LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The scene in which the info screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml");
/** /**
* The path to the FXML resource. * The path to the FXML resource.
@ -104,7 +82,7 @@ public final class SceneContext {
*/ */
public SceneContext(Stage stage) { public SceneContext(Stage stage) {
this.stage = stage; this.stage = stage;
EventBus.getInstance().register(ThemeChangeEvent.class, theme -> applyCSS()); EventBus.getInstance().registerListener(this);
} }
/** /**
@ -115,6 +93,7 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void load(SceneInfo sceneInfo) { public void load(SceneInfo sceneInfo) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
loader.setRoot(null); loader.setRoot(null);
loader.setController(null); loader.setController(null);
@ -125,8 +104,25 @@ public final class SceneContext {
sceneStack.push(scene); sceneStack.push(scene);
stage.setScene(scene); stage.setScene(scene);
applyCSS();
// Add the option to exit Linux-like with "Control" + "Q"
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
if (sceneInfo != SceneInfo.LOGIN_SCENE) scene.getAccelerators()
.put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), ShutdownHelper::logout);
// Add the option to open the settings scene with "Control"+"S", if being in
// chat scene
if (sceneInfo.equals(SceneInfo.CHAT_SCENE))
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), () -> load(SceneInfo.SETTINGS_SCENE));
// The LoginScene is the only scene not intended to be resized
// As strange as it seems, this is needed as otherwise the LoginScene won't be
// displayed on some OS (...Debian...)
stage.sizeToScene(); stage.sizeToScene();
Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE));
applyCSS();
stage.show(); stage.show();
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e); EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
@ -140,8 +136,12 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void pop() { public void pop() {
// Pop scene and controller
sceneStack.pop(); sceneStack.pop();
controllerStack.pop(); controllerStack.pop();
// Apply new scene if present
if (!sceneStack.isEmpty()) { if (!sceneStack.isEmpty()) {
final var newScene = sceneStack.peek(); final var newScene = sceneStack.peek();
stage.setScene(newScene); stage.setScene(newScene);
@ -164,6 +164,15 @@ public final class SceneContext {
} }
} }
@Event(eventType = Logout.class, priority = 150)
private void onLogout() {
sceneStack.clear();
controllerStack.clear();
}
@Event(priority = 150, eventType = ThemeChangeEvent.class)
private void onThemeChange() { applyCSS(); }
/** /**
* @param <T> the type of the controller * @param <T> the type of the controller
* @return the controller used by the current scene * @return the controller used by the current scene
@ -176,4 +185,10 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public Stage getStage() { return stage; } public Stage getStage() { return stage; }
/**
* @return whether the scene stack is empty
* @since Envoy Client v0.2-beta
*/
public boolean isEmpty() { return sceneStack.isEmpty(); }
} }

View File

@ -1,10 +1,9 @@
package envoy.client.ui; package envoy.client.ui;
import java.io.File; import java.io.*;
import java.io.IOException; import java.time.Instant;
import java.util.Properties; import java.util.concurrent.TimeoutException;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import javafx.application.Application; import javafx.application.Application;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
@ -12,22 +11,19 @@ import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene; import envoy.client.ui.controller.LoginScene;
import envoy.data.GroupMessage; import envoy.client.util.IconUtil;
import envoy.data.Message; import envoy.data.*;
import envoy.event.GroupMessageStatusChange; import envoy.data.User.UserStatus;
import envoy.event.MessageStatusChange; import envoy.event.*;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* Handles application startup and shutdown. * Handles application startup.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Startup.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -42,9 +38,10 @@ public final class Startup extends Application {
*/ */
public static final String VERSION = "0.1-beta"; public static final String VERSION = "0.1-beta";
private LocalDB localDB; private static LocalDB localDB;
private Client client;
private static final Context context = Context.getInstance();
private static final Client client = context.getClient();
private static final ClientConfig config = ClientConfig.getInstance(); private static final ClientConfig config = ClientConfig.getInstance();
private static final Logger logger = EnvoyLog.getLogger(Startup.class); private static final Logger logger = EnvoyLog.getLogger(Startup.class);
@ -56,80 +53,172 @@ public final class Startup extends Application {
*/ */
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
// Initialize config and logger
try { try {
// Load the configuration from client.properties first config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0]));
final Properties properties = new Properties(); EnvoyLog.initialize(config);
properties.load(Startup.class.getClassLoader().getResourceAsStream("client.properties")); } catch (final IllegalStateException e) {
config.load(properties);
// Override configuration values with command line arguments
final String[] args = getParameters().getRaw().toArray(new String[0]);
if (args.length > 0) config.load(args);
// Check if all mandatory configuration values have been initialized
if (!config.isInitialized()) throw new EnvoyException("Configuration is not fully initialized");
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e); new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
logger.log(Level.SEVERE, "Error loading configuration values: ", e); logger.log(Level.SEVERE, "Error loading configuration values: ", e);
e.printStackTrace();
System.exit(1); System.exit(1);
} }
// Setup logger for the envoy package
EnvoyLog.initialize(config);
EnvoyLog.attach("envoy");
EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier());
EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier());
logger.log(Level.INFO, "Envoy starting..."); logger.log(Level.INFO, "Envoy starting...");
// Initialize the local database // Initialize the local database
if (config.isIgnoreLocalDB()) { try {
localDB = new TransientLocalDB(); final var localDBFile = new File(config.getHomeDirectory(), config.getServer());
new Alert(AlertType.WARNING, "Ignoring local database.\nMessages will not be saved!").showAndWait(); logger.info("Initializing LocalDB at " + localDBFile);
} else try { localDB = new LocalDB(localDBFile);
localDB = new PersistentLocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath())); } catch (IOException | EnvoyException e) {
} catch (final IOException e3) { logger.log(Level.SEVERE, "Could not initialize local database: ", e);
logger.log(Level.SEVERE, "Could not initialize local database: ", e3); new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e3).showAndWait();
System.exit(1); System.exit(1);
return; return;
} }
// Initialize client and unread message cache // Prepare handshake
client = new Client(); context.setLocalDB(localDB);
// Configure stage
stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
// Create scene context
final var sceneContext = new SceneContext(stage);
context.setSceneContext(sceneContext);
// Authenticate with token if present
if (localDB.getAuthToken() != null) {
logger.info("Attempting authentication with token...");
localDB.loadUserData();
if (!performHandshake(
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
sceneContext.load(SceneInfo.LOGIN_SCENE);
} else
// Load login scene
sceneContext.load(SceneInfo.LOGIN_SCENE);
}
/**
* Tries to perform a Handshake with the server.
*
* @param credentials the credentials to use for the handshake
* @return whether the handshake was successful or offline mode could be entered
* @since Envoy Client v0.2-beta
*/
public static boolean performHandshake(LoginCredentials credentials) {
final var cacheMap = new CacheMap(); final var cacheMap = new CacheMap();
cacheMap.put(Message.class, new Cache<Message>()); cacheMap.put(Message.class, new Cache<Message>());
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>()); cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>()); cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>()); cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
try {
stage.setTitle("Envoy"); client.performHandshake(credentials, cacheMap);
stage.getIcons().add(IconUtil.loadIcon("envoy_logo")); if (client.isOnline()) {
loadChatScene();
final var sceneContext = new SceneContext(stage); client.initReceiver(localDB, cacheMap);
sceneContext.load(SceneInfo.LOGIN_SCENE); return true;
sceneContext.<LoginScene>getController().initializeData(client, localDB, cacheMap, sceneContext); } else return false;
} catch (IOException | InterruptedException | TimeoutException e) {
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
return attemptOfflineMode(credentials.getIdentifier());
}
} }
/** /**
* Closes the client connection and saves the local database and settings. * Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode
* for a given user.
* *
* @since Envoy Client v0.1-beta * @param identifier the identifier of the user - currently his username
* @return whether the offline mode could be entered
* @since Envoy Client v0.2-beta
*/ */
@Override public static boolean attemptOfflineMode(String identifier) {
public void stop() {
try { try {
logger.log(Level.INFO, "Closing connection..."); // Try entering offline mode
client.close(); final User clientUser = localDB.getUsers().get(identifier);
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
logger.log(Level.INFO, "Saving local database and settings..."); client.setSender(clientUser);
localDB.save(); loadChatScene();
Settings.getInstance().save(); return true;
logger.log(Level.INFO, "Envoy was terminated by its user");
} catch (final Exception e) { } catch (final Exception e) {
logger.log(Level.SEVERE, "Unable to save local files: ", e); new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
System.exit(1);
return false;
} }
} }
/**
* Loads the last known time a user has been online.
*
* @param identifier the identifier of this user - currently his name
* @return the last {@code Instant} at which he has been online
* @since Envoy Client v0.2-beta
*/
public static Instant loadLastSync(String identifier) {
try {
localDB.setUser(localDB.getUsers().get(identifier));
localDB.loadUserData();
} catch (final Exception e) {
// User storage empty, wrong user name etc. -> default lastSync
}
return localDB.getLastSync();
}
private static void loadChatScene() {
// Set client user in local database
localDB.setUser(client.getSender());
// Initialize chats in local database
try {
localDB.loadUserData();
} catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
logger.log(Level.WARNING, "Could not load local database: ", e);
}
context.initWriteProxy();
if (client.isOnline()) context.getWriteProxy().flushCache();
else
// Set all contacts to offline mode
localDB.getChats()
.stream()
.map(Chat::getRecipient)
.filter(User.class::isInstance)
.map(User.class::cast)
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
final var stage = context.getStage();
// Pop LoginScene if present
if (!context.getSceneContext().isEmpty()) context.getSceneContext().pop();
// Load ChatScene
stage.setMinHeight(400);
stage.setMinWidth(843);
context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE);
stage.centerOnScreen();
if (StatusTrayIcon.isSupported()) {
// Exit or minimize the stage when a close request occurs
stage.setOnCloseRequest(e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose()) e.consume(); });
// Initialize status tray icon
final var trayIcon = new StatusTrayIcon(stage);
Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> {
if ((Boolean) c) trayIcon.show();
else trayIcon.hide();
});
}
// Start auto save thread
localDB.initAutoSave();
}
} }

View File

@ -2,25 +2,22 @@ package envoy.client.ui;
import java.awt.*; import java.awt.*;
import java.awt.TrayIcon.MessageType; import java.awt.TrayIcon.MessageType;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.logging.Level;
import envoy.client.event.MessageCreationEvent; import javafx.application.Platform;
import javafx.stage.Stage;
import envoy.client.helper.ShutdownHelper;
import envoy.client.util.IconUtil;
import envoy.data.Message; import envoy.data.Message;
import envoy.event.EventBus;
import envoy.exception.EnvoyException; import dev.kske.eventbus.*;
import envoy.util.EnvoyLog; import dev.kske.eventbus.Event;
/** /**
* Project: <strong>envoy-client</strong><br>
* File: <strong>StatusTrayIcon.java</strong><br>
* Created: <strong>3 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public class StatusTrayIcon { public final class StatusTrayIcon implements EventListener {
/** /**
* The {@link TrayIcon} provided by the System Tray API for controlling the * The {@link TrayIcon} provided by the System Tray API for controlling the
@ -33,68 +30,68 @@ public class StatusTrayIcon {
* A received {@link Message} is only displayed as a system tray notification if * A received {@link Message} is only displayed as a system tray notification if
* this variable is set to {@code true}. * this variable is set to {@code true}.
*/ */
private boolean displayMessages = false; private boolean displayMessages;
/**
* @return {@code true} if the status tray icon is supported on this platform
* @since Envoy Client v0.2-beta
*/
public static boolean isSupported() { return SystemTray.isSupported(); }
/** /**
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
* menu. * menu.
* *
* @param focusTarget the {@link Window} which focus determines if message * @param stage the stage whose focus determines if message
* notifications are displayed * notifications are displayed
* @throws EnvoyException if the currently used OS does not support the System * @since Envoy Client v0.2-beta
* Tray API
* @since Envoy Client v0.2-alpha
*/ */
public StatusTrayIcon(Window focusTarget) throws EnvoyException { public StatusTrayIcon(Stage stage) {
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported."); trayIcon = new TrayIcon(IconUtil.loadAWTCompatible("/icons/envoy_logo.png"), "Envoy");
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
final Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
trayIcon = new TrayIcon(img, "Envoy Client");
trayIcon.setImageAutoSize(true); trayIcon.setImageAutoSize(true);
trayIcon.setToolTip("You are notified if you have unread messages."); trayIcon.setToolTip("You are notified if you have unread messages.");
final PopupMenu popup = new PopupMenu(); final PopupMenu popup = new PopupMenu();
final MenuItem exitMenuItem = new MenuItem("Exit"); final MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> System.exit(0)); exitMenuItem.addActionListener(evt -> ShutdownHelper.exit());
popup.add(exitMenuItem); popup.add(exitMenuItem);
trayIcon.setPopupMenu(popup); trayIcon.setPopupMenu(popup);
// Only display messages if the chat window is not focused // Only display messages if the stage is not focused
focusTarget.addWindowFocusListener(new WindowAdapter() { stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue());
@Override
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
@Override
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
});
// Show the window if the user clicks on the icon // Show the window if the user clicks on the icon
trayIcon.addActionListener(evt -> { focusTarget.setVisible(true); focusTarget.requestFocus(); }); trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); }));
// Start processing message events // Start processing message events
// TODO: Handle other message types EventBus.getInstance().registerListener(this);
EventBus.getInstance()
.register(MessageCreationEvent.class,
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
} }
/** /**
* Makes this {@link StatusTrayIcon} appear in the system tray. * Makes the icon appear in the system tray.
* *
* @throws EnvoyException if the status icon could not be attaches to the system
* tray for system-internal reasons
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void show() throws EnvoyException { public void show() {
try { try {
SystemTray.getSystemTray().add(trayIcon); SystemTray.getSystemTray().add(trayIcon);
} catch (final AWTException e) { } catch (final AWTException e) {}
EnvoyLog.getLogger(StatusTrayIcon.class).log(Level.INFO, "Could not display StatusTrayIcon: ", e); }
throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e);
} /**
* Removes the icon from the system tray.
*
* @since Envoy Client v0.2-beta
*/
public void hide() { SystemTray.getSystemTray().remove(trayIcon); }
@Event
private void onMessage(Message message) {
if (displayMessages) trayIcon.displayMessage(
message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received",
message.getText(),
MessageType.INFO);
} }
} }

View File

@ -1,11 +1,9 @@
package envoy.client.ui; package envoy.client.ui.control;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import envoy.client.data.audio.AudioPlayer; import envoy.client.data.audio.AudioPlayer;
@ -14,10 +12,6 @@ import envoy.util.EnvoyLog;
/** /**
* Enables the play back of audio clips through a button. * Enables the play back of audio clips through a button.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>AudioControl.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta

View File

@ -0,0 +1,69 @@
package envoy.client.ui.control;
import javafx.geometry.*;
import javafx.scene.control.Label;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import envoy.client.data.*;
import envoy.client.util.IconUtil;
/**
* Displays a chat using a contact control for the recipient and a label for the
* unread message count.
*
* @see ContactControl
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public final class ChatControl extends HBox {
private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32),
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
/**
* @param chat the chat to display
* @since Envoy Client v0.1-beta
*/
public ChatControl(Chat chat) {
setAlignment(Pos.CENTER_LEFT);
setPadding(new Insets(0, 0, 3, 0));
// Profile picture
ImageView contactProfilePic = new ImageView(chat instanceof GroupChat ? groupIcon : userIcon);
final var clip = new Rectangle();
clip.setWidth(32);
clip.setHeight(32);
clip.setArcHeight(32);
clip.setArcWidth(32);
contactProfilePic.setClip(clip);
getChildren().add(contactProfilePic);
// Spacing
final var leftSpacing = new Region();
leftSpacing.setPrefSize(8, 0);
leftSpacing.setMinSize(8, 0);
leftSpacing.setMaxSize(8, 0);
getChildren().add(leftSpacing);
// Contact control
getChildren().add(new ContactControl(chat.getRecipient()));
// Unread messages
if (chat.getUnreadAmount() != 0) {
final var spacing = new Region();
setHgrow(spacing, Priority.ALWAYS);
getChildren().add(spacing);
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
unreadMessagesLabel.setMinSize(15, 15);
final var vbox = new VBox();
vbox.setAlignment(Pos.CENTER_RIGHT);
unreadMessagesLabel.setAlignment(Pos.CENTER);
unreadMessagesLabel.getStyleClass().add("unread-messages-amount");
vbox.getChildren().add(unreadMessagesLabel);
getChildren().add(vbox);
}
getStyleClass().add("list-element");
}
}

View File

@ -0,0 +1,39 @@
package envoy.client.ui.control;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import envoy.data.*;
/**
* Displays information about a contact in two rows. The first row contains the
* name. The second row contains the online status (user) or the member count
* (group).
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-beta
*/
public final class ContactControl extends VBox {
/**
* @param contact the contact to display
* @since Envoy Client v0.2-beta
*/
public ContactControl(Contact contact) {
// Name label
final var nameLabel = new Label(contact.getName());
getChildren().add(nameLabel);
// Online status (user) or member count (group)
if (contact instanceof User) {
final var status = ((User) contact).getStatus().toString();
final var statusLabel = new Label(status);
statusLabel.getStyleClass().add(status.toLowerCase());
getChildren().add(statusLabel);
} else {
getChildren().add(new Label(contact.getContacts().size() + " members"));
}
getStyleClass().add("list-element");
}
}

View File

@ -0,0 +1,173 @@
package envoy.client.ui.control;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.*;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.*;
import javafx.geometry.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import envoy.client.data.*;
import envoy.client.ui.*;
import envoy.client.util.IconUtil;
import envoy.data.*;
import envoy.data.Message.MessageStatus;
import envoy.util.EnvoyLog;
/**
* This class transforms a single {@link Message} into a UI component.
*
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public final class MessageControl extends Label {
private final boolean ownMessage;
private final LocalDB localDB = Context.getInstance().getLocalDB();
private final SceneContext sceneContext = Context.getInstance().getSceneContext();
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
.withZone(ZoneId.systemDefault());
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
private static final Settings settings = Settings.getInstance();
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
/**
*
* @param message the message that should be formatted
* @since Envoy Client v0.1-beta
*/
public MessageControl(Message message) {
// Creating the underlying VBox and the dateLabel
final var hbox = new HBox();
if (message.getSenderID() != localDB.getUser().getID() && message instanceof GroupMessage) {
// Displaying the name of the sender in a group
final var label = new Label();
label.getStyleClass().add("group-member-names");
label.setText(localDB.getUsers()
.values()
.stream()
.filter(c -> c.getID() == message.getSenderID())
.findFirst()
.map(User::getName)
.orElse("Unknown User"));
label.setPadding(new Insets(0, 5, 0, 0));
hbox.getChildren().add(label);
}
hbox.getChildren().add(new Label(dateFormat.format(message.getCreationDate())));
final var vbox = new VBox(hbox);
// Creating the actions for the MenuItems
final var contextMenu = new ContextMenu();
final var copyMenuItem = new MenuItem("Copy");
final var deleteMenuItem = new MenuItem("Delete");
final var forwardMenuItem = new MenuItem("Forward");
final var quoteMenuItem = new MenuItem("Quote");
final var infoMenuItem = new MenuItem("Info");
copyMenuItem.setOnAction(e -> copyMessage(message));
deleteMenuItem.setOnAction(e -> deleteMessage(message));
forwardMenuItem.setOnAction(e -> forwardMessage(message));
quoteMenuItem.setOnAction(e -> quoteMessage(message));
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
// Handling message attachment display
// TODO: Add missing attachment types
if (message.hasAttachment()) {
switch (message.getAttachment().getType()) {
case PICTURE:
vbox.getChildren()
.add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
break;
case VIDEO:
break;
case VOICE:
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
break;
case DOCUMENT:
break;
}
final var saveAttachment = new MenuItem("Save attachment");
saveAttachment.setOnAction(e -> saveAttachment(message));
contextMenu.getItems().add(saveAttachment);
}
// Creating the textLabel
final var textLabel = new Label(message.getText());
textLabel.setMaxWidth(430);
textLabel.setWrapText(true);
final var hBoxBottom = new HBox();
hBoxBottom.getChildren().add(textLabel);
// Setting the message status icon and background color
if (message.getSenderID() == localDB.getUser().getID()) {
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
statusIcon.setPreserveRatio(true);
final var space = new Region();
HBox.setHgrow(space, Priority.ALWAYS);
hBoxBottom.getChildren().add(space);
hBoxBottom.getChildren().add(statusIcon);
hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT);
getStyleClass().add("own-message");
ownMessage = true;
hbox.setAlignment(Pos.CENTER_RIGHT);
} else {
getStyleClass().add("received-message");
ownMessage = false;
}
vbox.getChildren().add(hBoxBottom);
// Adjusting height and weight of the cell to the corresponding ListView
paddingProperty().setValue(new Insets(5, 20, 5, 20));
setContextMenu(contextMenu);
setGraphic(vbox);
}
// Context Menu actions
private void copyMessage(Message message) {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
}
private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
private void saveAttachment(Message message) {
File file;
final var fileName = message.getAttachment().getName();
final var downloadLocation = settings.getDownloadLocation();
// Show save file dialog, if the user did not opt-out
if (!settings.isDownloadSavedWithoutAsking()) {
final var fileChooser = new FileChooser();
fileChooser.setInitialFileName(fileName);
fileChooser.setInitialDirectory(downloadLocation);
file = fileChooser.showSaveDialog(sceneContext.getStage());
} else file = new File(downloadLocation, fileName);
// A file was selected
if (file != null) try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(message.getAttachment().getData());
logger.log(Level.FINE, "Attachment of message was saved at " + file.getAbsolutePath());
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e);
}
}
/**
* @return whether the message stored by this {@code MessageControl} has been
* sent by this user of Envoy
* @since Envoy Client v0.1-beta
*/
public boolean isOwnMessage() { return ownMessage; }
}

View File

@ -0,0 +1,56 @@
package envoy.client.ui.control;
import javafx.scene.image.*;
import javafx.scene.shape.Rectangle;
/**
* Provides a set of convenience constructors for images that are displayed as profile pictures.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class ProfilePicImageView extends ImageView {
/**
* Creates a new {@code ProfilePicImageView} without a default image.
*
* @since Envoy Client v0.2-beta
*/
public ProfilePicImageView() { this(null); }
/**
* Creates a new {@code ProfilePicImageView}.
*
* @param image the image to display
* @since Envoy Client v0.2-beta
*/
public ProfilePicImageView(Image image) { this(image, 40); }
/**
* Creates a new {@code ProfilePicImageView}.
*
* @param image the image to display
* @param sizeAndRounding the size and rounding for a circular
* {@code ProfilePicImageView}
* @since Envoy Client v0.2-beta
*/
public ProfilePicImageView(Image image, double sizeAndRounding) { this(image, sizeAndRounding, sizeAndRounding); }
/**
* Creates a new {@code ProfilePicImageView}.
*
* @param image the image to display
* @param size the size of this {@code ProfilePicImageView}
* @param rounding how rounded this {@code ProfilePicImageView} should be
* @since Envoy Client v0.2-beta
*/
public ProfilePicImageView(Image image, double size, double rounding) {
super(image);
final var clip = new Rectangle();
clip.setWidth(size);
clip.setHeight(size);
clip.setArcHeight(rounding);
clip.setArcWidth(rounding);
setClip(clip);
}
}

View File

@ -0,0 +1,105 @@
package envoy.client.ui.control;
import java.util.function.Consumer;
import javafx.event.*;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
/**
* Displays a context menu that offers an additional option when one of
* its menu items has been clicked.
* <p>
* Current options are:
* <ul>
* <li>undo</li>
* <li>redo</li>
* <li>cut</li>
* <li>copy</li>
* <li>paste</li>
* <li>delete</li>
* <li>clear</li>
* <li>Select all</li>
* </ul>
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
* @apiNote please refrain from using
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already
* used by this component
*/
public class TextInputContextMenu extends ContextMenu {
private final MenuItem undoMI = new MenuItem("Undo");
private final MenuItem redoMI = new MenuItem("Redo");
private final MenuItem cutMI = new MenuItem("Cut");
private final MenuItem copyMI = new MenuItem("Copy");
private final MenuItem pasteMI = new MenuItem("Paste");
private final MenuItem deleteMI = new MenuItem("Delete selection");
private final MenuItem clearMI = new MenuItem("Clear");
private final MenuItem selectAllMI = new MenuItem("Select all");
private final MenuItem separatorMI = new SeparatorMenuItem();
/**
* Creates a new {@code TextInputContextMenu} with an optional action when
* this menu was clicked. Currently shows:
* <ul>
* <li>undo</li>
* <li>redo</li>
* <li>cut</li>
* <li>copy</li>
* <li>paste</li>
* <li>delete</li>
* <li>clear</li>
* <li>Select all</li>
* </ul>
*
* @param control the text input component to display this
* {@code ContextMenu}
* @param menuItemClicked the second action to perform when a menu item of this
* context menu has been clicked
* @since Envoy Client v0.2-beta
* @apiNote please refrain from using
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already
* used by this component
*/
public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) {
// Define the actions when clicked
undoMI.setOnAction(addAction(e -> control.undo(), menuItemClicked));
redoMI.setOnAction(addAction(e -> control.redo(), menuItemClicked));
cutMI.setOnAction(addAction(e -> control.cut(), menuItemClicked));
copyMI.setOnAction(addAction(e -> control.copy(), menuItemClicked));
pasteMI.setOnAction(addAction(e -> control.paste(), menuItemClicked));
deleteMI.setOnAction(addAction(e -> control.replaceSelection(""), menuItemClicked));
clearMI.setOnAction(addAction(e -> control.setText(""), menuItemClicked));
selectAllMI.setOnAction(addAction(e -> control.selectAll(), menuItemClicked));
// Define the times it will be disabled
undoMI.disableProperty().bind(control.undoableProperty().not());
redoMI.disableProperty().bind(control.redoableProperty().not());
cutMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
copyMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
deleteMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
clearMI.disableProperty().bind(control.textProperty().isEmpty());
setOnShowing(e -> pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString()));
selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
// Add all items to the ContextMenu
getItems().add(undoMI);
getItems().add(redoMI);
getItems().add(cutMI);
getItems().add(copyMI);
getItems().add(pasteMI);
getItems().add(separatorMI);
getItems().add(deleteMI);
getItems().add(clearMI);
getItems().add(separatorMI);
getItems().add(selectAllMI);
}
private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction, Consumer<ActionEvent> additionalAction) {
return e -> { originalAction.accept(e); additionalAction.accept(e); };
}
}

View File

@ -0,0 +1,9 @@
/**
* Defines custom UI controls.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.2-beta
*/
package envoy.client.ui.control;

View File

@ -2,57 +2,57 @@ package envoy.client.ui.controller;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random; import java.util.Random;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.collections.transformation.FilteredList;
import javafx.scene.Node; import javafx.fxml.*;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image; import javafx.scene.image.*;
import javafx.scene.image.ImageView; import javafx.scene.input.*;
import javafx.scene.input.KeyCode; import javafx.scene.layout.*;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration; import javafx.util.Duration;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
import envoy.client.event.MessageCreationEvent; import envoy.client.data.commands.*;
import envoy.client.net.Client; import envoy.client.event.*;
import envoy.client.net.WriteProxy; import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.IconUtil; import envoy.client.net.*;
import envoy.client.ui.Restorable; import envoy.client.ui.*;
import envoy.client.ui.SceneContext; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.listcell.ContactListCellFactory; import envoy.client.ui.control.*;
import envoy.client.ui.listcell.MessageControl; import envoy.client.ui.listcell.*;
import envoy.client.ui.listcell.MessageListCellFactory; import envoy.client.util.*;
import envoy.data.*; import envoy.data.*;
import envoy.data.Attachment.AttachmentType; import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.ContactOperation; import envoy.event.contact.ContactOperation;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
/** /**
* Project: <strong>envoy-client</strong><br> * Controller for the chat scene.
* File: <strong>ChatSceneController.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class ChatScene implements Restorable { public final class ChatScene implements EventListener, Restorable {
@FXML @FXML
private GridPane scene; private GridPane scene;
@ -79,7 +79,13 @@ public final class ChatScene implements Restorable {
private Button settingsButton; private Button settingsButton;
@FXML @FXML
private Button rotateButton; private Button messageSearchButton;
@FXML
private Button newGroupButton;
@FXML
private Button newContactButton;
@FXML @FXML
private TextArea messageTextArea; private TextArea messageTextArea;
@ -96,24 +102,59 @@ public final class ChatScene implements Restorable {
@FXML @FXML
private ImageView attachmentView; private ImageView attachmentView;
private LocalDB localDB; @FXML
private Client client; private Label topBarContactLabel;
private WriteProxy writeProxy;
private SceneContext sceneContext;
private Chat currentChat; @FXML
private AudioRecorder recorder; private Label topBarStatusLabel;
private boolean recording;
private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled;
private static final Settings settings = Settings.getInstance(); @FXML
private static final EventBus eventBus = EventBus.getInstance(); private ImageView clientProfilePic;
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
private static final Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20); @FXML
private static final int MAX_MESSAGE_LENGTH = 255; private ImageView recipientProfilePic;
private static final int DEFAULT_ICON_SIZE = 16;
@FXML
private TextArea contactSearch;
@FXML
private VBox contactOperations;
@FXML
private TabPane tabPane;
@FXML
private Tab contactSearchTab;
@FXML
private Tab groupCreationTab;
@FXML
private HBox contactSpecificOnlineOperations;
private Chat currentChat;
private FilteredList<Chat> chats;
private boolean recording;
private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled;
private boolean isCustomAttachmentImage;
private final LocalDB localDB = context.getLocalDB();
private final Client client = context.getClient();
private final WriteProxy writeProxy = context.getWriteProxy();
private final SceneContext sceneContext = context.getSceneContext();
private final AudioRecorder recorder = new AudioRecorder();
private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this");
private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
private static final Context context = Context.getInstance();
private static final int MAX_MESSAGE_LENGTH = 255;
private static final int DEFAULT_ICON_SIZE = 16;
/** /**
* Initializes the appearance of certain visual components. * Initializes the appearance of certain visual components.
@ -122,105 +163,179 @@ public final class ChatScene implements Restorable {
*/ */
@FXML @FXML
private void initialize() { private void initialize() {
eventBus.registerListener(this);
// Initialize message and user rendering // Initialize message and user rendering
messageList.setCellFactory(MessageListCellFactory::new); messageList.setCellFactory(MessageListCell::new);
chatList.setCellFactory(ContactListCellFactory::new); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
// JavaFX provides an internal way of populating the context menu of a text
// area.
// We, however, need additional functionality.
messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
// Set the icons of buttons and image views
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE))); attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE); attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5)))); messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
onlyIfOnlineTooltip.setShowDelay(Duration.millis(250));
final var clip = new Rectangle();
clip.setWidth(43);
clip.setHeight(43);
clip.setArcHeight(43);
clip.setArcWidth(43);
clientProfilePic.setClip(clip);
// Listen to received messages chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
eventBus.register(MessageCreationEvent.class, e -> { contactLabel.setText(localDB.getUser().getName());
final var message = e.get();
localDB.getChat(message instanceof GroupMessage ? message.getRecipientID() : message.getSenderID()).ifPresent(chat -> {
chat.insert(message);
if (chat.equals(currentChat)) {
try {
currentChat.read(writeProxy);
} catch (final IOException e1) {
logger.log(Level.WARNING, "Could not read current chat: ", e1);
}
Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); });
} else chat.incrementUnreadAmount();
// Moving chat with most recent unreadMessages to the top
Platform.runLater(() -> {
chatList.getItems().remove(chat);
chatList.getItems().add(0, chat);
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
localDB.getChats().remove(chat);
localDB.getChats().add(0, chat);
});
});
});
// Listen to message status changes initializeSystemCommandsMap();
eventBus.register(MessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> {
message.setStatus(e.get());
// Update UI if in current chat
if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
}));
eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> { Platform.runLater(() -> {
((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get()); final var online = client.isOnline();
// no check will be performed in case it has already been disabled - a negative
// Update UI if in current chat // GroupCreationResult might have been returned
if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh); if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online);
})); newContactButton.setDisable(!online);
if (online) try {
// Listen to user status changes Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
eventBus.register(UserStatusChange.class, contactSearchTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml")));
e -> chatList.getItems() groupCreationTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml")));
.stream() } catch (final IOException e) {
.filter(c -> c.getRecipient().getID() == e.getID()) logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e);
.findAny() }
.map(Chat::getRecipient) else {
.ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); })); Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
updateInfoLabel("You are offline", "info-label-warning");
// Listen to contacts changes
eventBus.register(ContactOperation.class, e -> {
final var contact = e.get();
switch (e.getOperationType()) {
case ADD:
localDB.getUsers().put(contact.getName(), contact);
Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
localDB.getChats().add(chat);
Platform.runLater(() -> chatList.getItems().add(chat));
break;
case REMOVE:
localDB.getUsers().remove(contact.getName());
localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID());
Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().getID() == contact.getID()));
break;
} }
}); });
} }
@Event(eventType = BackEvent.class)
private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); }
@Event(includeSubtypes = true)
private void onMessage(Message message) {
// The sender of the message is the recipient of the chat
// Exceptions: this user is the sender (sync) or group message (group is
// recipient)
final boolean ownMessage = message.getSenderID() == localDB.getUser().getID();
final var recipientID = message instanceof GroupMessage || ownMessage ? message.getRecipientID() : message.getSenderID();
localDB.getChat(recipientID).ifPresent(chat -> {
chat.insert(message);
// Read current chat or increment unread amount
if (chat.equals(currentChat)) {
currentChat.read(writeProxy);
Platform.runLater(this::scrollToMessageListEnd);
} else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount();
// Move chat with most recent unread messages to the top
Platform.runLater(() -> {
chats.getSource().remove(chat);
((ObservableList<Chat>) chats.getSource()).add(0, chat);
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
});
});
}
@Event
private void onMessageStatusChange(MessageStatusChange evt) {
// Update UI if in current chat and the current user was the sender of the
// message
if (currentChat != null) localDB.getMessage(evt.getID())
.filter(msg -> msg.getSenderID() == client.getSender().getID())
.ifPresent(msg -> Platform.runLater(messageList::refresh));
}
@Event(eventType = UserStatusChange.class)
private void onUserStatusChange() { Platform.runLater(chatList::refresh); }
@Event
private void onContactOperation(ContactOperation operation) {
final var contact = operation.get();
switch (operation.getOperationType()) {
case ADD:
if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact);
final var chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
Platform.runLater(() -> ((ObservableList<Chat>) chats.getSource()).add(0, chat));
break;
case REMOVE:
Platform.runLater(() -> chats.getSource().removeIf(c -> c.getRecipient().equals(contact)));
break;
}
}
@Event(eventType = NoAttachments.class)
private void onNoAttachments() {
Platform.runLater(() -> {
attachmentButton.setDisable(true);
voiceButton.setDisable(true);
final var alert = new Alert(AlertType.ERROR);
alert.setTitle("No attachments possible");
alert.setHeaderText("Your current server does not support attachments.");
alert.setContentText("If this is unplanned, please contact your server administrator.");
alert.showAndWait();
});
}
@Event
private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(!result.get())); }
@Event(eventType = ThemeChangeEvent.class)
private void onThemeChange() {
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
attachmentView.setImage(isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE);
messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
messageList.setCellFactory(MessageListCell::new);
// TODO: cache image
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
}
@Event(eventType = Logout.class, priority = 200)
private void onLogout() { eventBus.removeListener(this); }
/** /**
* Initializes all necessary data via dependency injection- * Initializes all {@code SystemCommands} used in {@code ChatScene}.
* *
* @param sceneContext the scene context used to load other scenes * @since Envoy Client v0.2-beta
* @param localDB the local database form which chats and users are loaded
* @param client the client used to request ID generators
* @param writeProxy the write proxy used to send messages and other data to
* the server
* @since Envoy Client v0.1-beta
*/ */
public void initializeData(SceneContext sceneContext, LocalDB localDB, Client client, WriteProxy writeProxy) { private void initializeSystemCommandsMap() {
this.sceneContext = sceneContext; final var builder = new SystemCommandBuilder(messageTextAreaCommands);
this.localDB = localDB;
this.client = client;
this.writeProxy = writeProxy;
chatList.setItems(FXCollections.observableList(localDB.getChats())); // Do A Barrel roll initialization
contactLabel.setText(localDB.getUser().getName()); final var random = new Random();
MessageControl.setUser(localDB.getUser()); builder.setAction(text -> doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1))))
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); .setDefaults(Integer.toString(random.nextInt(3) + 1), Double.toString(random.nextDouble() * 3 + 1))
.setDescription("See for yourself :)")
.setNumberOfArguments(2)
.build("dabr");
recorder = new AudioRecorder(); // Logout initialization
builder.setAction(text -> ShutdownHelper.logout()).setNumberOfArguments(0).setDescription("Logs you out.").build("logout");
// Exit initialization
builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0).setDescription("Exits the program").build("exit", false);
builder.build("q");
// Open settings scene initialization
builder.setAction(text -> sceneContext.load(SceneInfo.SETTINGS_SCENE))
.setNumberOfArguments(0)
.setDescription("Opens the settings screen")
.build("settings");
} }
@Override @Override
@ -233,7 +348,9 @@ public final class ChatScene implements Restorable {
*/ */
@FXML @FXML
private void chatListClicked() { private void chatListClicked() {
final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient(); if (chatList.getSelectionModel().isEmpty()) return;
final var user = chatList.getSelectionModel().getSelectedItem().getRecipient();
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
@ -241,18 +358,14 @@ public final class ChatScene implements Restorable {
// Load the chat // Load the chat
currentChat = localDB.getChat(user.getID()).get(); currentChat = localDB.getChat(user.getID()).get();
messageList.setItems(FXCollections.observableList(currentChat.getMessages())); messageList.setItems(currentChat.getMessages());
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1; final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount();
messageList.scrollTo(scrollIndex); messageList.scrollTo(scrollIndex);
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
deleteContactMenuItem.setText("Delete " + user.getName()); deleteContactMenuItem.setText("Delete " + user.getName());
// Read the current chat // Read the current chat
try { currentChat.read(writeProxy);
currentChat.read(writeProxy);
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not read current chat.", e);
}
// Discard the pending attachment // Discard the pending attachment
if (recorder.isRecording()) { if (recorder.isRecording()) {
@ -270,6 +383,27 @@ public final class ChatScene implements Restorable {
voiceButton.setDisable(!recorder.isSupported()); voiceButton.setDisable(!recorder.isSupported());
attachmentButton.setDisable(false); attachmentButton.setDisable(false);
chatList.refresh(); chatList.refresh();
if (currentChat != null) {
topBarContactLabel.setText(currentChat.getRecipient().getName());
if (currentChat.getRecipient() instanceof User) {
final String status = ((User) currentChat.getRecipient()).getStatus().toString();
topBarStatusLabel.setText(status);
topBarStatusLabel.getStyleClass().add(status.toLowerCase());
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
} else {
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members");
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
}
final Rectangle clip = new Rectangle();
clip.setWidth(43);
clip.setHeight(43);
clip.setArcHeight(43);
clip.setArcWidth(43);
recipientProfilePic.setClip(clip);
messageSearchButton.setVisible(true);
}
} }
/** /**
@ -278,10 +412,7 @@ public final class ChatScene implements Restorable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void settingsButtonClicked() { private void settingsButtonClicked() { sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); }
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
}
/** /**
* Actions to perform when the "Add Contact" - Button has been clicked. * Actions to perform when the "Add Contact" - Button has been clicked.
@ -289,10 +420,10 @@ public final class ChatScene implements Restorable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void addContactButtonClicked() { private void addContactButtonClicked() { tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal()); }
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB); @FXML
} private void groupCreationButtonClicked() { tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal()); }
@FXML @FXML
private void voiceButtonClicked() { private void voiceButtonClicked() {
@ -306,7 +437,9 @@ public final class ChatScene implements Restorable {
}); });
recorder.start(); recorder.start();
} else { } else {
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE); pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_"
+ DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss").format(LocalDateTime.now()) + "." + AudioRecorder.FILE_FORMAT,
AttachmentType.VOICE);
recording = false; recording = false;
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
@ -344,7 +477,7 @@ public final class ChatScene implements Restorable {
} }
// Get attachment type (default is document) // Get attachment type (default is document)
AttachmentType type = AttachmentType.DOCUMENT; var type = AttachmentType.DOCUMENT;
switch (fileChooser.getSelectedExtensionFilter().getDescription()) { switch (fileChooser.getSelectedExtensionFilter().getDescription()) {
case "Pictures": case "Pictures":
type = AttachmentType.PICTURE; type = AttachmentType.PICTURE;
@ -357,10 +490,13 @@ public final class ChatScene implements Restorable {
// Create the pending attachment // Create the pending attachment
try { try {
final var fileBytes = Files.readAllBytes(file.toPath()); final var fileBytes = Files.readAllBytes(file.toPath());
pendingAttachment = new Attachment(fileBytes, type); pendingAttachment = new Attachment(fileBytes, file.getName(), type);
checkPostConditions(false);
// Setting the preview image as image of the attachmentView // Setting the preview image as image of the attachmentView
if (type == AttachmentType.PICTURE) if (type == AttachmentType.PICTURE) {
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true)); attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
isCustomAttachmentImage = true;
}
attachmentView.setVisible(true); attachmentView.setVisible(true);
} catch (final IOException e) { } catch (final IOException e) {
new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait(); new Alert(AlertType.ERROR, "The selected file could not be loaded!").showAndWait();
@ -369,20 +505,27 @@ public final class ChatScene implements Restorable {
} }
/** /**
* Rotates every element in our application by 360° in at most 2.75s. * Rotates every element in our application by {@code rotations}*360° in
* {@code an}.
* *
* @param rotations the amount of times the scene is rotated by 360°
* @param animationTime the time in seconds that this animation lasts
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML private void doABarrelRoll(int rotations, double animationTime) {
private void doABarrelRoll() { // Limiting the rotations and duration
// contains all Node objects in ChatScene in alphabetical order rotations = Math.min(rotations, 100000);
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea, rotations = Math.max(rotations, 1);
postButton, remainingChars, rotateButton, scene, settingsButton, chatList, voiceButton }; animationTime = Math.min(animationTime, 150);
final var random = new Random(); animationTime = Math.max(animationTime, 0.25);
// contains all Node objects in ChatScene
final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this);
for (final var node : rotatableNodes) { for (final var node : rotatableNodes) {
// Defines at most four whole rotation in at most 4s // Sets the animation duration to {animationTime}
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node); final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node);
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360); // rotates every element {rotations} times
rotateTransition.setByAngle(rotations * 360);
rotateTransition.play(); rotateTransition.play();
// This is needed as for some strange reason objects could stop before being // This is needed as for some strange reason objects could stop before being
// rotated back to 0° // rotated back to 0°
@ -400,10 +543,35 @@ public final class ChatScene implements Restorable {
*/ */
@FXML @FXML
private void checkKeyCombination(KeyEvent e) { private void checkKeyCombination(KeyEvent e) {
// Checks whether the text is too long // Checks whether the text is too long
messageTextUpdated(); messageTextUpdated();
// Automatic sending of messages via (ctrl +) enter
checkPostConditions(e); // Sending an IsTyping event if none has been sent for
// IsTyping#millisecondsActive
if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) {
client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID()));
currentChat.lastWritingEventWasNow();
}
// KeyPressed will be called before the char has been added to the text, hence
// this is needed for the first char
if (messageTextArea.getText().length() == 1 && e != null) checkPostConditions(e);
// This is needed for the messageTA context menu
else if (e == null) checkPostConditions(false);
}
/**
* Returns the id that should be used to send things to the server: the id of
* 'our' {@link User} if the recipient of that object is another User, else the
* id of the {@link Group} 'our' user is sending to.
*
* @return an id that can be sent to the server
* @since Envoy Client v0.2-beta
*/
private long getChatID() {
return currentChat.getRecipient() instanceof User ? client.getSender().getID() : currentChat.getRecipient().getID();
} }
/** /**
@ -412,20 +580,37 @@ public final class ChatScene implements Restorable {
*/ */
@FXML @FXML
private void checkPostConditions(KeyEvent e) { private void checkPostConditions(KeyEvent e) {
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER final var enterPressed = e.getCode() == KeyCode.ENTER;
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()); final var messagePosted = enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown() : false;
if (messagePosted) {
// Removing an inserted line break if added by pressing enter
final var text = messageTextArea.getText();
final var textPosition = messageTextArea.getCaretPosition() - 1;
if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n')
messageTextArea.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
}
// if control is pressed, the enter press is originally invalidated. Here it'll
// be inserted again
else if (enterPressed && e.isControlDown()) {
var caretPosition = messageTextArea.getCaretPosition();
messageTextArea.setText(new StringBuilder(messageTextArea.getText()).insert(caretPosition, '\n').toString());
messageTextArea.positionCaret(++caretPosition);
}
checkPostConditions(messagePosted);
} }
private void checkPostConditions(boolean sendKeyPressed) { private void checkPostConditions(boolean postMessage) {
if (!postingPermanentlyDisabled) { if (!postingPermanentlyDisabled) {
if (!postButton.isDisabled() && sendKeyPressed) postMessage(); if (!postButton.isDisabled() && postMessage) postMessage();
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null); postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
} else { } else {
final var noMoreMessaging = "Go online to send messages"; final var noMoreMessaging = "Go online to send messages";
if (!infoLabel.getText().equals(noMoreMessaging)) if (!infoLabel.getText().equals(noMoreMessaging))
// Informing the user that he is a f*cking moron and should use Envoy online // Informing the user that he is a f*cking moron and should use Envoy online
// because he ran out of messageIDs to use // because he ran out of messageIDs to use
updateInfoLabel(noMoreMessaging, "infoLabel-error"); updateInfoLabel(noMoreMessaging, "info-label-error");
} }
} }
@ -451,8 +636,8 @@ public final class ChatScene implements Restorable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void updateRemainingCharsLabel() { private void updateRemainingCharsLabel() {
final int currentLength = messageTextArea.getText().length(); final var currentLength = messageTextArea.getText().length();
final int remainingLength = MAX_MESSAGE_LENGTH - currentLength; final var remainingLength = MAX_MESSAGE_LENGTH - currentLength;
remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH)); remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1)); remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
} }
@ -470,11 +655,11 @@ public final class ChatScene implements Restorable {
postButton.setDisable(true); postButton.setDisable(true);
messageTextArea.setDisable(true); messageTextArea.setDisable(true);
messageTextArea.clear(); messageTextArea.clear();
updateInfoLabel("You need to go online to send more messages", "infoLabel-error"); updateInfoLabel("You need to go online to send more messages", "info-label-error");
return; return;
} }
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
try { if (!messageTextAreaCommands.executeIfAnyPresent(text)) {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text); .setText(text);
@ -495,27 +680,23 @@ public final class ChatScene implements Restorable {
currentChat.insert(message); currentChat.insert(message);
// Moving currentChat to the top // Moving currentChat to the top
Platform.runLater(() -> { Platform.runLater(() -> {
chatList.getItems().remove(currentChat); chats.getSource().remove(currentChat);
chatList.getItems().add(0, currentChat); ((ObservableList<Chat>) chats.getSource()).add(0, currentChat);
chatList.getSelectionModel().select(0); chatList.getSelectionModel().select(0);
localDB.getChats().remove(currentChat); localDB.getChats().remove(currentChat);
localDB.getChats().add(0, currentChat); localDB.getChats().add(0, currentChat);
}); });
messageList.refresh();
scrollToMessageListEnd(); scrollToMessageListEnd();
// Request a new ID generator if all IDs were used // Request a new ID generator if all IDs were used
if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIdGenerator(); if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIDGenerator();
} catch (final IOException e) {
logger.log(Level.SEVERE, "Error while sending message: ", e);
new Alert(AlertType.ERROR, "An error occured while sending the message!").showAndWait();
} }
// Clear text field and disable post button // Clear text field and disable post button
messageTextArea.setText(""); messageTextArea.setText("");
postButton.setDisable(true); postButton.setDisable(true);
updateRemainingCharsLabel(); updateRemainingCharsLabel();
isCustomAttachmentImage = false;
} }
/** /**
@ -541,9 +722,8 @@ public final class ChatScene implements Restorable {
/** /**
* Updates the {@code attachmentView} in terms of visibility.<br> * Updates the {@code attachmentView} in terms of visibility.<br>
* Additionally resets the shown image to * Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE}
* {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image is currently * if another image is currently present.
* present.
* *
* @param visible whether the {@code attachmentView} should be displayed * @param visible whether the {@code attachmentView} should be displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -562,9 +742,20 @@ public final class ChatScene implements Restorable {
private void copyAndPostMessage() { private void copyAndPostMessage() {
final var messageText = messageTextArea.getText(); final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
final var image = attachmentView.getImage();
final var messageAttachment = pendingAttachment;
postMessage(); postMessage();
messageTextArea.setText(messageText); messageTextArea.setText(messageText);
updateRemainingCharsLabel(); updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank()); postButton.setDisable(messageText.isBlank());
attachmentView.setImage(image);
if (attachmentView.getImage() != null) attachmentView.setVisible(true);
pendingAttachment = messageAttachment;
}
@FXML
private void searchContacts() {
chats.setPredicate(contactSearch.getText().isBlank() ? c -> true
: c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase()));
} }
} }

View File

@ -1,130 +0,0 @@
package envoy.client.ui.controller;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ListView;
import envoy.client.data.Chat;
import envoy.client.data.LocalDB;
import envoy.client.event.SendEvent;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.listcell.ContactListCellFactory;
import envoy.event.ElementOperation;
import envoy.event.EventBus;
import envoy.event.contact.ContactOperation;
import envoy.event.contact.ContactSearchRequest;
import envoy.event.contact.ContactSearchResult;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ContactSearchSceneController.java</strong><br>
* Created: <strong>07.06.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class ContactSearchScene {
@FXML
private ClearableTextField searchBar;
@FXML
private ListView<Chat> chatList;
private SceneContext sceneContext;
private LocalDB localDB;
private static EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
/**
* @param sceneContext enables the user to return to the chat scene
* @param localDB the local database to which new contacts are added
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
this.sceneContext = sceneContext;
this.localDB = localDB;
}
@FXML
private void initialize() {
chatList.setCellFactory(ContactListCellFactory::new);
searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); });
eventBus.register(ContactSearchResult.class,
response -> Platform.runLater(() -> {
chatList.getItems().clear();
chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList()));
}));
}
/**
* Disables the clear and search button if no text is present in the search bar.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void sendRequest() {
final var text = searchBar.getTextField().getText().strip();
if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text)));
else chatList.getItems().clear();
}
/**
* Clears the text in the search bar and the items shown in the list.
* Additionally disables both clear and search button.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void clear() {
searchBar.getTextField().setText(null);
chatList.getItems().clear();
}
/**
* Sends an {@link ContactOperation} for every selected contact to the
* server.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
final var chat = chatList.getSelectionModel().getSelectedItem();
if (chat != null) {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Add Contact to Contact List");
alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?");
// Normally, this would be total BS (we are already on the FX Thread), however
// it could be proven that the creation of this dialog wrapped in
// Platform.runLater is less error-prone than without it
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD);
// Sends the event to the server
eventBus.dispatch(new SendEvent(event));
// Updates the UI
eventBus.dispatch(event);
logger.log(Level.INFO, "Added contact " + chat.getRecipient());
}));
}
}
@FXML
private void newGroupButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.GROUP_CREATION_SCENE);
sceneContext.<GroupCreationScene>getController().initializeData(sceneContext, localDB);
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
}

View File

@ -0,0 +1,128 @@
package envoy.client.ui.controller;
import java.util.logging.*;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.Context;
import envoy.client.event.BackEvent;
import envoy.client.helper.AlertHelper;
import envoy.client.net.Client;
import envoy.client.ui.control.ContactControl;
import envoy.client.ui.listcell.ListCellFactory;
import envoy.data.User;
import envoy.event.ElementOperation;
import envoy.event.contact.*;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
/**
* Provides a search bar in which a user name (substring) can be entered. The
* users with a matching name are then displayed inside a list view. A
* {@link UserSearchRequest} is sent on every keystroke.
* <p>
* <i>The actual search algorithm is implemented on the server.
* <p>
* To create a group, a button is available that loads the
* {@link GroupCreationTab}.
*
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class ContactSearchTab implements EventListener {
@FXML
private TextArea searchBar;
@FXML
private ListView<User> userList;
private User currentlySelectedUser;
private final Alert alert = new Alert(AlertType.CONFIRMATION);
private static final Client client = Context.getInstance().getClient();
private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
@FXML
private void initialize() {
eventBus.registerListener(this);
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
alert.setTitle("Add User?");
}
@Event
private void onUserSearchResult(UserSearchResult result) {
Platform.runLater(() -> { userList.getItems().clear(); userList.getItems().addAll(result.get()); });
}
@Event
private void onContactOperation(ContactOperation operation) {
final var contact = operation.get();
if (operation.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> {
userList.getItems().remove(contact);
if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close();
});
}
/**
* If text is present, sends a request to the server.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void sendRequest() {
final var text = searchBar.getText().strip();
if (!text.isBlank()) client.send(new UserSearchRequest(text));
else userList.getItems().clear();
}
/**
* Clears the text in the search bar and the items shown in the list.
* Additionally disables both clear and search button.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void clear() {
searchBar.setText(null);
userList.getItems().clear();
}
/**
* Sends an {@link ContactOperation} for the selected user to the
* server.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void userListClicked() {
final var user = userList.getSelectionModel().getSelectedItem();
if (user != null) {
currentlySelectedUser = user;
alert.setContentText("Add user " + currentlySelectedUser.getName() + " to your contacts?");
AlertHelper.confirmAction(alert, this::addAsContact);
}
}
private void addAsContact() {
// Sends the event to the server
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
client.send(event);
// Removes the chosen user and updates the UI
userList.getItems().remove(currentlySelectedUser);
eventBus.dispatch(event);
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
}
@FXML
private void backButtonClicked() { eventBus.dispatch(new BackEvent()); }
}

View File

@ -1,109 +0,0 @@
package envoy.client.ui.controller;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.Chat;
import envoy.client.data.LocalDB;
import envoy.client.event.SendEvent;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.listcell.ContactListCellFactory;
import envoy.data.Group;
import envoy.event.EventBus;
import envoy.event.GroupCreation;
import envoy.util.Bounds;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ContactSearchSceneController.java</strong><br>
* Created: <strong>07.06.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupCreationScene {
@FXML
private Button createButton;
@FXML
private ClearableTextField groupNameField;
@FXML
private ListView<Chat> chatList;
private SceneContext sceneContext;
private static final EventBus eventBus = EventBus.getInstance();
@FXML
private void initialize() {
chatList.setCellFactory(ContactListCellFactory::new);
chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); });
}
/**
* @param sceneContext enables the user to return to the chat scene
* @param localDB the local database from which potential group members can
* be selected
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
this.sceneContext = sceneContext;
Platform.runLater(() -> chatList.getItems()
.addAll(localDB.getChats()
.stream()
.filter(c -> !(c.getRecipient() instanceof Group))
.filter(c -> c.getRecipient().getID() != localDB.getUser().getID())
.collect(Collectors.toList())));
}
/**
* Enables the {@code createButton} if at least one contact is selected.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank());
}
/**
* Checks, whether the {@code createButton} can be enabled because text is
* present in the textfield.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void textUpdated() { createButton.setDisable(groupNameField.getTextField().getText().isBlank()); }
/**
* Sends a {@link GroupCreation} to the server and closes this scene.
* <p>
* If the given group name is not valid, an error is displayed instead.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void createButtonClicked() {
final var name = groupNameField.getTextField().getText();
if (!Bounds.isValidContactName(name)) {
new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
groupNameField.getTextField().clear();
} else {
eventBus.dispatch(new SendEvent(new GroupCreation(name,
chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet()))));
new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait();
sceneContext.pop();
}
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
}

View File

@ -0,0 +1,212 @@
package envoy.client.ui.controller;
import static java.util.function.Predicate.not;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import envoy.client.data.*;
import envoy.client.event.BackEvent;
import envoy.client.ui.control.ContactControl;
import envoy.client.ui.listcell.ListCellFactory;
import envoy.data.*;
import envoy.event.GroupCreation;
import envoy.event.contact.ContactOperation;
import envoy.util.Bounds;
import dev.kske.eventbus.*;
/**
* Provides a group creation interface. A group name can be entered in the text
* field at the top. Available users (local chat recipients) are displayed
* inside a list and can be selected (multiple selection available).
* <p>
* When the group creation button is pressed, a {@link GroupCreation} is sent to
* the server. This controller enforces a valid group name and a non-empty
* member list (excluding the client user).
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupCreationTab implements EventListener {
@FXML
private Button createButton;
@FXML
private Button cancelButton;
@FXML
private TextArea groupNameField;
@FXML
private ListView<User> userList;
@FXML
private Label errorMessageLabel;
@FXML
private Button proceedDuplicateButton;
@FXML
private Button cancelDuplicateButton;
@FXML
private HBox errorProceedBox;
private String name;
private final LocalDB localDB = Context.getInstance().getLocalDB();
private static final EventBus eventBus = EventBus.getInstance();
@FXML
private void initialize() {
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
userList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
createButton.setDisable(true);
eventBus.registerListener(this);
userList.getItems()
.addAll(localDB.getChats()
.stream()
.map(Chat::getRecipient)
.filter(User.class::isInstance)
.filter(not(localDB.getUser()::equals))
.map(User.class::cast)
.collect(Collectors.toList()));
}
/**
* Enables the {@code createButton} if at least one contact is selected.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void userListClicked() { createButton.setDisable(userList.getSelectionModel().isEmpty() || groupNameField.getText().isBlank()); }
/**
* Checks, whether the {@code createButton} can be enabled because text is
* present in the text field.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void textUpdated() { createButton.setDisable(userList.getSelectionModel().isEmpty() || groupNameField.getText().isBlank()); }
/**
* Sends a {@link GroupCreation} to the server and closes this scene.
* <p>
* If the given group name is not valid, an error is displayed instead.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void createButtonClicked() {
name = groupNameField.getText();
if (!Bounds.isValidContactName(name)) {
setErrorMessageLabelSize(30);
errorMessageLabel.setText("The group name is not valid!");
groupNameField.clear();
} else if (groupNameAlreadyPresent(name)) {
setErrorMessageLabelSize(30);
errorMessageLabel.setText("Name does already exist! Proceed anyways?");
setProcessPaneSize(30);
createButton.setDisable(true);
cancelButton.setDisable(true);
} else {
createGroup(name);
eventBus.dispatch(new BackEvent());
// Restoring the original design as tabs will always be reused
setErrorMessageLabelSize(0);
groupNameField.clear();
}
}
/**
* Creates a new group with the given name and all selected members.<br>
* Additionally pops the scene automatically.
*
* @param name the chosen group name
* @since Envoy Client v0.1-beta
*/
private void createGroup(String name) {
Context.getInstance()
.getClient()
.send(new GroupCreation(name, userList.getSelectionModel().getSelectedItems().stream().map(User::getID).collect(Collectors.toSet())));
}
/**
* Returns true if the proposed group name is already present in the users
* {@code LocalDB}.
*
* @param newName the chosen group name
* @return true if this name is already present
* @since Envoy Client v0.1-beta
*/
public boolean groupNameAlreadyPresent(String newName) {
return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance).map(Contact::getName).anyMatch(newName::equals);
}
@FXML
private void backButtonClicked() {
eventBus.dispatch(new BackEvent());
setErrorMessageLabelSize(0);
setProcessPaneSize(0);
}
@FXML
private void proceedOnNameDuplication() {
createButton.setDisable(false);
cancelButton.setDisable(false);
createGroup(name);
eventBus.dispatch(new BackEvent());
setErrorMessageLabelSize(0);
setProcessPaneSize(0);
groupNameField.clear();
}
@FXML
private void cancelOnNameDuplication() {
createButton.setDisable(false);
cancelButton.setDisable(false);
setErrorMessageLabelSize(0);
setProcessPaneSize(0);
groupNameField.clear();
}
private void setErrorMessageLabelSize(int value) {
errorMessageLabel.setPrefHeight(value);
errorMessageLabel.setMinHeight(value);
errorMessageLabel.setMaxHeight(value);
}
private void setProcessPaneSize(int value) {
proceedDuplicateButton.setPrefHeight(value);
proceedDuplicateButton.setMinHeight(value);
proceedDuplicateButton.setMaxHeight(value);
cancelDuplicateButton.setPrefHeight(value);
cancelDuplicateButton.setMinHeight(value);
cancelDuplicateButton.setMaxHeight(value);
errorProceedBox.setPrefHeight(value);
errorProceedBox.setMinHeight(value);
errorProceedBox.setMaxHeight(value);
}
@Event
private void onContactOperation(ContactOperation operation) {
if (operation.get() instanceof User) Platform.runLater(() -> {
switch (operation.getOperationType()) {
case ADD:
userList.getItems().add((User) operation.get());
break;
case REMOVE:
userList.getItems().removeIf(operation.get()::equals);
break;
}
});
}
}

View File

@ -1,44 +1,35 @@
package envoy.client.ui.controller; package envoy.client.ui.controller;
import java.io.FileNotFoundException; import java.time.Instant;
import java.io.IOException; import java.util.logging.*;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.ImageView;
import envoy.client.data.*; import envoy.client.data.ClientConfig;
import envoy.client.net.Client; import envoy.client.ui.*;
import envoy.client.net.WriteProxy; import envoy.client.util.IconUtil;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.Startup;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.data.User;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import envoy.event.HandshakeRejection; import envoy.event.HandshakeRejection;
import envoy.exception.EnvoyException; import envoy.util.*;
import envoy.util.Bounds;
import envoy.util.EnvoyLog; import dev.kske.eventbus.*;
/** /**
* Project: <strong>envoy-client</strong><br> * Controller for the login scene.
* File: <strong>LoginDialog.java</strong><br>
* Created: <strong>03.04.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class LoginScene { public final class LoginScene implements EventListener {
@FXML @FXML
private ClearableTextField userTextField; private TextField userTextField;
@FXML @FXML
private PasswordField passwordField; private PasswordField passwordField;
@ -47,81 +38,86 @@ public final class LoginScene {
private PasswordField repeatPasswordField; private PasswordField repeatPasswordField;
@FXML @FXML
private Label repeatPasswordLabel; private Button registerSwitch;
@FXML
private CheckBox registerCheckBox;
@FXML @FXML
private Label connectionLabel; private Label connectionLabel;
private Client client; @FXML
private LocalDB localDB; private Button loginButton;
private CacheMap cacheMap;
private SceneContext sceneContext;
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class); @FXML
private static final EventBus eventBus = EventBus.getInstance(); private CheckBox cbStaySignedIn;
private static final ClientConfig config = ClientConfig.getInstance();
@FXML
private Button offlineModeButton;
@FXML
private Label registerTextLabel;
@FXML
private ImageView logo;
private boolean registration;
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
private static final ClientConfig config = ClientConfig.getInstance();
@FXML @FXML
private void initialize() { private void initialize() {
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort()); connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
// Show an alert after an unsuccessful handshake // Show an alert after an unsuccessful handshake
eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); })); EventBus.getInstance().registerListener(this);
}
/** logo.setImage(IconUtil.loadIcon("envoy_logo"));
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
*
* @param client the client used to perform the handshake
* @param localDB the local database used for offline login
* @param cacheMap the map of all caches needed
* @param sceneContext the scene context used to initialize the chat scene
* @since Envoy Client v0.1-beta
*/
public void initializeData(Client client, LocalDB localDB, CacheMap cacheMap, SceneContext sceneContext) {
this.client = client;
this.localDB = localDB;
this.cacheMap = cacheMap;
this.sceneContext = sceneContext;
// Prepare handshake
localDB.loadIDGenerator();
// Set initial cursor // Set initial cursor
userTextField.requestFocus(); userTextField.requestFocus();
// Perform automatic login if configured
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
} }
@FXML @FXML
private void loginButtonPressed() { private void loginButtonPressed() {
final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText();
final boolean requestToken = cbStaySignedIn.isSelected();
// Prevent registration with unequal passwords // Prevent registration with unequal passwords
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) { if (registration && !pass.equals(repeatPass)) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait(); new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
repeatPasswordField.clear(); repeatPasswordField.clear();
} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) { } else if (!Bounds.isValidContactName(user)) {
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait(); new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
userTextField.getTextField().clear(); userTextField.clear();
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(), } else {
Startup.VERSION)); Instant lastSync = Startup.loadLastSync(userTextField.getText());
Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync)
: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
}
} }
@FXML @FXML
private void offlineModeButtonPressed() { private void offlineModeButtonPressed() { Startup.attemptOfflineMode(userTextField.getText()); }
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
}
@FXML @FXML
private void registerCheckboxChanged() { private void registerSwitchPressed() {
// Update button text and register switch
if (!registration) {
loginButton.setText("Register");
loginButton.setPadding(new Insets(2, 116, 2, 116));
registerTextLabel.setText("Already an account?");
registerSwitch.setText("Login");
} else {
loginButton.setText("Login");
loginButton.setPadding(new Insets(2, 125, 2, 125));
registerTextLabel.setText("No account yet?");
registerSwitch.setText("Register");
}
registration = !registration;
// Make repeat password field and label visible / invisible // Make repeat password field and label visible / invisible
repeatPasswordField.setVisible(registerCheckBox.isSelected()); repeatPasswordField.setVisible(registration);
repeatPasswordLabel.setVisible(registerCheckBox.isSelected()); offlineModeButton.setDisable(registration);
} }
@FXML @FXML
@ -130,70 +126,6 @@ public final class LoginScene {
System.exit(0); System.exit(0);
} }
private void performHandshake(LoginCredentials credentials) { @Event
try { private void onHandshakeRejection(HandshakeRejection evt) { Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait()); }
client.performHandshake(credentials, cacheMap);
if (client.isOnline()) {
loadChatScene();
client.initReceiver(localDB, cacheMap);
}
} catch (IOException | InterruptedException | TimeoutException e) {
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
attemptOfflineMode(credentials);
}
}
private void attemptOfflineMode(LoginCredentials credentials) {
try {
// Try entering offline mode
localDB.loadUsers();
final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser);
loadChatScene();
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
System.exit(1);
}
}
private void loadChatScene() {
// Set client user in local database
localDB.setUser(client.getSender());
// Initialize chats in local database
try {
localDB.initializeUserStorage();
localDB.loadUserData();
} catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
logger.log(Level.WARNING, "Could not load local database: ", e);
}
// Initialize write proxy
final var writeProxy = new WriteProxy(client, localDB);
localDB.synchronize();
if (client.isOnline()) writeProxy.flushCache();
else
// Set all contacts to offline mode
localDB.getChats()
.stream()
.map(Chat::getRecipient)
.filter(User.class::isInstance)
.map(User.class::cast)
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
// Load ChatScene
sceneContext.pop();
sceneContext.getStage().setMinHeight(400);
sceneContext.getStage().setMinWidth(350);
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
}
} }

View File

@ -3,19 +3,17 @@ package envoy.client.ui.controller;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import envoy.client.ui.SceneContext; import envoy.client.data.Context;
import envoy.client.ui.settings.GeneralSettingsPane; import envoy.client.ui.listcell.ListCellFactory;
import envoy.client.ui.settings.SettingsPane; import envoy.client.ui.settings.*;
/** /**
* Project: <strong>envoy-client</strong><br> * Controller for the settings scene.
* File: <strong>SettingsSceneController.java</strong><br>
* Created: <strong>10.04.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class SettingsScene { public final class SettingsScene {
@FXML @FXML
private ListView<SettingsPane> settingsList; private ListView<SettingsPane> settingsList;
@ -23,26 +21,10 @@ public class SettingsScene {
@FXML @FXML
private TitledPane titledPane; private TitledPane titledPane;
private SceneContext sceneContext;
/**
* @param sceneContext enables the user to return to the chat scene
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
@FXML @FXML
private void initialize() { private void initialize() {
settingsList.setCellFactory(listView -> new ListCell<>() { settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle())));
settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(), new DownloadSettingsPane(), new BugReportPane());
@Override
protected void updateItem(SettingsPane item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) setGraphic(new Label(item.getTitle()));
}
});
settingsList.getItems().add(new GeneralSettingsPane());
} }
@FXML @FXML
@ -55,5 +37,5 @@ public class SettingsScene {
} }
@FXML @FXML
private void backButtonClicked() { sceneContext.pop(); } private void backButtonClicked() { Context.getInstance().getSceneContext().pop(); }
} }

View File

@ -0,0 +1,25 @@
package envoy.client.ui.controller;
/**
* Provides options to select different tabs.
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.2-beta
*/
public enum Tabs {
/**
* Selects the {@code contact list} tab.
*/
CONTACT_LIST,
/**
* Selects the {@code contact search} tab.
*/
CONTACT_SEARCH,
/**
* Selects the {@code group creation} tab.
*/
GROUP_CREATION
}

View File

@ -1,11 +1,9 @@
/** /**
* Contains JavaFX scene controllers. * Contains JavaFX scene controllers.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>08.06.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
package envoy.client.ui.controller; package envoy.client.ui.controller;

View File

@ -0,0 +1,47 @@
package envoy.client.ui.listcell;
import javafx.scene.*;
import javafx.scene.control.*;
/**
* Provides a convenience frame for list cell creation.
*
* @author Kai S. K. Engelbart
* @param <T> the type of element displayed by the list cell
* @param <U> the type of node as which the list element will be displayed
* @since Envoy Client v0.1-beta
*/
public abstract class AbstractListCell<T, U extends Node> extends ListCell<T> {
protected ListView<? extends T> listView;
/**
* @param listView the list view inside of which the cell will be displayed
* @since Envoy Client v0.1-beta
*/
public AbstractListCell(ListView<? extends T> listView) {
this.listView = listView;
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
getStyleClass().add("list-element");
}
@Override
protected final void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!(empty || item == null)) {
setCursor(Cursor.HAND);
setGraphic(renderItem(item));
} else {
setGraphic(null);
}
}
/**
* Converts a list item to a node. This can have side effects on the list cell.
*
* @param item the item to render
* @return a node representing the item
* @since Envoy Client v0.1-beta
*/
protected abstract U renderItem(T item);
}

View File

@ -1,58 +0,0 @@
package envoy.client.ui.listcell;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import envoy.client.data.Chat;
import envoy.data.Contact;
import envoy.data.Group;
import envoy.data.User;
/**
* This class formats a single {@link Contact} into a UI component.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ContactControl.java</strong><br>
* Created: <strong>01.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class ChatControl extends HBox {
/**
* @param chat the chat to display
* @since Envoy Client v0.1-beta
*/
public ChatControl(Chat chat) {
// Container with contact name
final var vBox = new VBox();
final var nameLabel = new Label(chat.getRecipient().getName());
nameLabel.setWrapText(true);
vBox.getChildren().add(nameLabel);
if (chat.getRecipient() instanceof User) {
// Online status
final var user = (User) chat.getRecipient();
final var statusLabel = new Label(user.getStatus().toString());
statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase());
vBox.getChildren().add(statusLabel);
} else // Member count
vBox.getChildren().add(new Label(((Group) chat.getRecipient()).getContacts().size() + " members"));
getChildren().add(vBox);
if (chat.getUnreadAmount() != 0) {
Region spacing = new Region();
setHgrow(spacing, Priority.ALWAYS);
getChildren().add(spacing);
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
unreadMessagesLabel.setMinSize(15, 15);
var vBox2 = new VBox();
vBox2.setAlignment(Pos.CENTER_RIGHT);
unreadMessagesLabel.setAlignment(Pos.CENTER);
unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");
vBox2.getChildren().add(unreadMessagesLabel);
getChildren().add(vBox2);
}
}
}

View File

@ -1,44 +0,0 @@
package envoy.client.ui.listcell;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import envoy.client.data.Chat;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>UserListCell.java</strong><br>
* Created: <strong>28.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class ContactListCellFactory extends ListCell<Chat> {
private final ListView<Chat> listView;
/**
* @param listView the list view inside which this cell is contained
* @since Envoy Client v0.1-beta
*/
public ContactListCellFactory(ListView<Chat> listView) { this.listView = listView; }
/**
* Displays the name of a contact. If the contact is a user, their online status
* is displayed as well.
*
* @since Envoy Client v0.1-beta
*/
@Override
protected void updateItem(Chat chat, boolean empty) {
super.updateItem(chat, empty);
if (empty || chat.getRecipient() == null) {
setText(null);
setGraphic(null);
} else {
final var control = new ChatControl(chat);
prefWidthProperty().bind(listView.widthProperty().subtract(40));
setGraphic(control);
}
}
}

View File

@ -0,0 +1,32 @@
package envoy.client.ui.listcell;
import java.util.function.Function;
import javafx.scene.Node;
import javafx.scene.control.ListView;
/**
* A generic list cell rendering an item using a provided render function.
*
* @author Kai S. K. Engelbart
* @param <T> the type of element displayed by the list cell
* @param <U> the type of node as which the list element will be displayed
* @since Envoy Client v0.2-beta
*/
public final class GenericListCell<T, U extends Node> extends AbstractListCell<T, U> {
private final Function<? super T, U> renderer;
/**
* @param listView the list view inside of which the cell will be displayed
* @param renderer a function converting a list item to a node
* @since Envoy Client v0.1-beta
*/
public GenericListCell(ListView<? extends T> listView, Function<? super T, U> renderer) {
super(listView);
this.renderer = renderer;
}
@Override
protected U renderItem(T item) { return renderer.apply(item); }
}

View File

@ -0,0 +1,30 @@
package envoy.client.ui.listcell;
import java.util.function.Function;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.util.Callback;
/**
* Provides a creation mechanism for generic list cells given a list view and a
* conversion function.
*
* @author Kai S. K. Engelbart
* @param <T> the type of object to display
* @param <U> the type of node displayed
* @since Envoy Client v0.1-beta
*/
public final class ListCellFactory<T, U extends Node> implements Callback<ListView<T>, ListCell<T>> {
private final Function<? super T, U> renderer;
/**
* @param renderer a function converting the type to display into a node
* @since Envoy Client v0.1-beta
*/
public ListCellFactory(Function<? super T, U> renderer) { this.renderer = renderer; }
@Override
public ListCell<T> call(ListView<T> listView) { return new GenericListCell<>(listView, renderer); }
}

View File

@ -1,123 +0,0 @@
package envoy.client.ui.listcell;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.geometry.Insets;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import envoy.client.ui.AudioControl;
import envoy.client.ui.IconUtil;
import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.data.User;
import envoy.util.EnvoyLog;
/**
* This class formats a single {@link Message} into a UI component.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageControl.java</strong><br>
* Created: <strong>01.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class MessageControl extends Label {
private static User client;
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
/**
*
* @param message the message that should be formatted
* @since Envoy Client v0.1-beta
*/
public MessageControl(Message message) {
// Creating the underlying VBox and the dateLabel
final var vbox = new VBox(new Label(dateFormat.format(message.getCreationDate())));
// Creating the actions for the MenuItems
final ContextMenu contextMenu = new ContextMenu();
final MenuItem copyMenuItem = new MenuItem("Copy");
final MenuItem deleteMenuItem = new MenuItem("Delete");
final MenuItem forwardMenuItem = new MenuItem("Forward");
final MenuItem quoteMenuItem = new MenuItem("Quote");
final MenuItem infoMenuItem = new MenuItem("Info");
copyMenuItem.setOnAction(e -> copyMessage(message));
deleteMenuItem.setOnAction(e -> deleteMessage(message));
forwardMenuItem.setOnAction(e -> forwardMessage(message));
quoteMenuItem.setOnAction(e -> quoteMessage(message));
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
// Handling message attachment display
if (message.hasAttachment()) {
switch (message.getAttachment().getType()) {
case PICTURE:
vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
break;
case VIDEO:
break;
case VOICE:
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
break;
case DOCUMENT:
break;
}
final var saveAttachment = new MenuItem("Save attachment");
saveAttachment.setOnAction(e -> saveAttachment(message));
contextMenu.getItems().add(saveAttachment);
}
// Creating the textLabel
final var textLabel = new Label(message.getText());
textLabel.setWrapText(true);
vbox.getChildren().add(textLabel);
// Setting the message status icon and background color
if (message.getSenderID() == client.getID()) {
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
statusIcon.setPreserveRatio(true);
vbox.getChildren().add(statusIcon);
getStyleClass().add("own-message");
} else getStyleClass().add("received-message");
// Adjusting height and weight of the cell to the corresponding ListView
paddingProperty().setValue(new Insets(5, 20, 5, 20));
setContextMenu(contextMenu);
setGraphic(vbox);
}
// Context Menu actions
private void copyMessage(Message message) {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
}
private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
private void saveAttachment(Message message) { logger.log(Level.FINEST, "attachment saving was requested for " + message); }
/**
* @param client the user who has logged in
* @since Envoy Client v0.1-beta
*/
public static void setUser(User client) { MessageControl.client = client; }
}

View File

@ -0,0 +1,37 @@
package envoy.client.ui.listcell;
import javafx.geometry.*;
import javafx.scene.control.ListView;
import envoy.client.ui.control.MessageControl;
import envoy.data.Message;
/**
* A list cell containing messages represented as message controls.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class MessageListCell extends AbstractListCell<Message, MessageControl> {
/**
* @param listView the list view inside of which the cell will be displayed
* @since Envoy Client v0.1-beta
*/
public MessageListCell(ListView<? extends Message> listView) { super(listView); }
@Override
protected MessageControl renderItem(Message message) {
final var control = new MessageControl(message);
listView.widthProperty().addListener((observable, oldValue, newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage()));
adjustPadding((int) listView.getWidth(), control.isOwnMessage());
if (control.isOwnMessage()) setAlignment(Pos.CENTER_RIGHT);
else setAlignment(Pos.CENTER_LEFT);
return control;
}
private void adjustPadding(int listWidth, boolean ownMessage) {
int padding = 10 + Math.max((listWidth - 1000) / 2, 0);
setPadding(ownMessage ? new Insets(0, padding, 6, 0) : new Insets(0, 0, 6, padding));
}
}

View File

@ -1,52 +0,0 @@
package envoy.client.ui.listcell;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Tooltip;
import javafx.stage.PopupWindow.AnchorLocation;
import envoy.data.Message;
/**
* Displays a single message inside the message list.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageListCellFactory.java</strong><br>
* Created: <strong>28.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class MessageListCellFactory extends ListCell<Message> {
private final ListView<Message> listView;
/**
* @param listView the list view inside which this cell is contained
* @since Envoy Client v0.1-beta
*/
public MessageListCellFactory(ListView<Message> listView) { this.listView = listView; }
/**
* Displays the text, the data of creation and the status of a message.
*
* @since Envoy v0.1-beta
*/
@Override
protected void updateItem(Message message, boolean empty) {
super.updateItem(message, empty);
if (empty || message == null) {
setText(null);
setGraphic(null);
} else {
final var control = new MessageControl(message);
control.prefWidthProperty().bind(listView.widthProperty().subtract(40));
// Creating the Tooltip to deselect a message
final var tooltip = new Tooltip("You can select a message by clicking on it \nand deselect it by pressing \"ctrl\" and clicking on it");
tooltip.setWrapText(true);
tooltip.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
setTooltip(tooltip);
setGraphic(control);
}
}
}

View File

@ -1,12 +1,9 @@
/** /**
* This package contains custom list cells that are used to display certain * This package contains custom list cells that are used to display certain
* things. * things.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>30.06.2020</strong><br>
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
package envoy.client.ui.listcell; package envoy.client.ui.listcell;

View File

@ -0,0 +1,66 @@
package envoy.client.ui.settings;
import javafx.event.EventHandler;
import javafx.scene.control.*;
import javafx.scene.input.InputEvent;
import envoy.event.IssueProposal;
/**
* This class offers the option for users to submit a bug report. Only the title
* of a bug is needed to be sent.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class BugReportPane extends OnlineOnlySettingsPane {
private final Label titleLabel = new Label("Suggest a title for the bug:");
private final TextField titleTextField = new TextField();
private final Label pleaseExplainLabel = new Label("Paste here the log of what went wrong and/ or explain what went wrong:");
private final TextArea errorDetailArea = new TextArea();
private final CheckBox showUsernameInBugReport = new CheckBox("Show your username in the bug report?");
private final Button submitReportButton = new Button("Submit report");
private final EventHandler<? super InputEvent> inputEventHandler = e -> submitReportButton.setDisable(titleTextField.getText().isBlank());
/**
* Creates a new {@code BugReportPane}.
*
* @since Envoy Client v0.2-beta
*/
public BugReportPane() {
super("Report a bug");
setSpacing(10);
setToolTipText("A bug can only be reported while being online");
// Displaying the label to ask for a title
titleLabel.setWrapText(true);
getChildren().add(titleLabel);
// Displaying the TextField where to enter the title of this bug
titleTextField.setOnKeyTyped(inputEventHandler);
titleTextField.setOnInputMethodTextChanged(inputEventHandler);
getChildren().add(titleTextField);
// Displaying the label to ask for clarification
pleaseExplainLabel.setWrapText(true);
getChildren().add(pleaseExplainLabel);
// Displaying the TextArea where to enter the log and/or own description
errorDetailArea.setWrapText(true);
getChildren().add(errorDetailArea);
// Displaying the consent button that your user name will be shown
showUsernameInBugReport.setSelected(true);
getChildren().add(showUsernameInBugReport);
// Displaying the submitReportButton
submitReportButton.setDisable(true);
submitReportButton.setOnAction(e -> {
String title = titleTextField.getText(), description = errorDetailArea.getText();
client.send(showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true) : new IssueProposal(title, description, client.getSender().getName(), true));
});
getChildren().add(submitReportButton);
}
}

View File

@ -0,0 +1,60 @@
package envoy.client.ui.settings;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.stage.DirectoryChooser;
import envoy.client.data.Context;
/**
* Displays options for downloading {@link envoy.data.Attachment}s.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class DownloadSettingsPane extends SettingsPane {
/**
* Constructs a new {@code DownloadSettingsPane}.
*
* @since Envoy Client v0.2-beta
*/
public DownloadSettingsPane() {
super("Download");
setSpacing(15);
setPadding(new Insets(15));
// Checkbox to disable asking
final var checkBox = new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
checkBox.setSelected(settings.isDownloadSavedWithoutAsking());
checkBox.setTooltip(new Tooltip("Determines whether a \"Select save location\" - dialogue will be shown when saving attachments."));
checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected()));
getChildren().add(checkBox);
// Displaying the default path to save to
final var pathLabel = new Label(settings.getItems().get("downloadLocation").getDescription() + ":");
pathLabel.setWrapText(true);
getChildren().add(pathLabel);
final var hbox = new HBox(20);
Tooltip.install(hbox, new Tooltip("Determines the location where attachments will be saved to."));
final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath());
hbox.getChildren().add(currentPath);
// Setting the default path
final var button = new Button("Select");
button.setOnAction(e -> {
final var directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Select the directory where attachments should be saved to");
directoryChooser.setInitialDirectory(settings.getDownloadLocation());
final var selectedDirectory = directoryChooser.showDialog(Context.getInstance().getSceneContext().getStage());
if (selectedDirectory != null) {
currentPath.setText(selectedDirectory.getAbsolutePath());
settings.setDownloadLocation(selectedDirectory);
}
});
hbox.getChildren().add(button);
getChildren().add(hbox);
}
}

View File

@ -1,57 +1,69 @@
package envoy.client.ui.settings; package envoy.client.ui.settings;
import java.util.List; import javafx.scene.control.*;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import envoy.client.data.Settings;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.helper.ShutdownHelper;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import dev.kske.eventbus.EventBus;
/** /**
* Project: <strong>envoy-client</strong><br>
* File: <strong>GeneralSettingsPane.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class GeneralSettingsPane extends SettingsPane { public final class GeneralSettingsPane extends SettingsPane {
private static final Settings settings = Settings.getInstance();
/** /**
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public GeneralSettingsPane() { public GeneralSettingsPane() {
super("General"); super("General");
final var vbox = new VBox(); setSpacing(10);
// TODO: Support other value types // TODO: Support other value types
List.of("onCloseMode", "enterToSend") final var settingsItems = settings.getItems();
.stream() final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
.map(settings.getItems()::get) final var hideOnCloseTooltip = new Tooltip("If selected, Envoy will still be present in the task bar when closed.");
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i)) hideOnCloseTooltip.setWrapText(true);
.forEach(vbox.getChildren()::add); hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
getChildren().add(hideOnCloseCheckbox);
final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
final var enterToSendTooltip = new Tooltip(
"When selected, messages can be sent pressing \"Enter\". A line break can be inserted by pressing \"Ctrl\" + \"Enter\". Else it will be the other way around.");
enterToSendTooltip.setWrapText(true);
enterToSendCheckbox.setTooltip(enterToSendTooltip);
getChildren().add(enterToSendCheckbox);
final var askForConfirmationCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation"));
final var askForConfirmationTooltip = new Tooltip("When selected, nothing will prompt a confirmation dialog");
askForConfirmationTooltip.setWrapText(true);
askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip);
getChildren().add(askForConfirmationCheckbox);
final var combobox = new ComboBox<String>(); final var combobox = new ComboBox<String>();
combobox.getItems().add("dark"); combobox.getItems().add("dark");
combobox.getItems().add("light"); combobox.getItems().add("light");
combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
combobox.setValue(settings.getCurrentTheme()); combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction( combobox.setOnAction(e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); }); getChildren().add(combobox);
vbox.getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>(); final var statusComboBox = new ComboBox<UserStatus>();
statusComboBox.getItems().setAll(UserStatus.values()); statusComboBox.getItems().setAll(UserStatus.values());
statusComboBox.setValue(UserStatus.ONLINE); statusComboBox.setValue(UserStatus.ONLINE);
statusComboBox.setTooltip(new Tooltip("Change your current status"));
// TODO add action when value is changed // TODO add action when value is changed
statusComboBox.setOnAction(e -> {}); statusComboBox.setOnAction(e -> {});
vbox.getChildren().add(statusComboBox); getChildren().add(statusComboBox);
getChildren().add(vbox); final var logoutButton = new Button("Logout");
logoutButton.setOnAction(e -> ShutdownHelper.logout());
final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account");
logoutTooltip.setWrapText(true);
logoutButton.setTooltip(logoutTooltip);
getChildren().add(logoutButton);
} }
} }

View File

@ -0,0 +1,54 @@
package envoy.client.ui.settings;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import envoy.client.data.Context;
import envoy.client.net.Client;
/**
* Inheriting from this class signifies that options should only be available if
* the {@link envoy.data.User} is currently online. If the user is currently
* offline, all {@link javafx.scene.Node} variables will be disabled and a
* {@link Tooltip} will be displayed for the whole node.
*
* @author Leon Hofmeister
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-beta
*/
public abstract class OnlineOnlySettingsPane extends SettingsPane {
protected final Client client = Context.getInstance().getClient();
private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your account.");
/**
* @param title the title of this pane
* @since Envoy Client v0.2-beta
*/
protected OnlineOnlySettingsPane(String title) {
super(title);
setDisable(!client.isOnline());
if (!client.isOnline()) {
final var infoLabel = new Label("You shall not pass!\n(... Unless you would happen to be online)");
infoLabel.setId("info-label-warning");
infoLabel.setWrapText(true);
getChildren().add(infoLabel);
setBackground(new Background(new BackgroundFill(Color.grayRgb(100, 0.3), CornerRadii.EMPTY, Insets.EMPTY)));
Tooltip.install(this, beOnlineReminder);
} else Tooltip.uninstall(this, beOnlineReminder);
}
/**
* Sets the text of the tooltip displayed for this pane.
*
* @param text the text to display
* @since Envoy Client v0.2-beta
*/
protected void setToolTipText(String text) { beOnlineReminder.setText(text); }
}

View File

@ -6,10 +6,6 @@ import javafx.scene.control.CheckBox;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
/** /**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsToggleButton.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */

View File

@ -1,19 +1,19 @@
package envoy.client.ui.settings; package envoy.client.ui.settings;
import javafx.scene.layout.Pane; import javafx.scene.layout.VBox;
import envoy.client.data.Settings;
/** /**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsPane.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public abstract class SettingsPane extends Pane { public abstract class SettingsPane extends VBox {
protected String title; protected String title;
protected static final Settings settings = Settings.getInstance();
protected SettingsPane(String title) { this.title = title; } protected SettingsPane(String title) { this.title = title; }
/** /**

View File

@ -0,0 +1,177 @@
package envoy.client.ui.settings;
import java.io.*;
import java.nio.file.Files;
import java.util.logging.*;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.*;
import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import envoy.client.data.Context;
import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil;
import envoy.event.*;
import envoy.util.*;
import dev.kske.eventbus.EventBus;
/**
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class UserSettingsPane extends OnlineOnlySettingsPane {
private boolean profilePicChanged, usernameChanged, validPassword;
private byte[] currentImageBytes;
private String newUsername, newPassword = "";
private final ImageView profilePic = new ProfilePicImageView(null, 60);
private final TextField usernameTextField = new TextField();
private final PasswordField currentPasswordField = new PasswordField();
private final PasswordField newPasswordField = new PasswordField();
private final PasswordField repeatNewPasswordField = new PasswordField();
private final Button saveButton = new Button("Save");
private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
/**
* Creates a new {@code UserSettingsPane}.
*
* @since Envoy Client v0.2-beta
*/
public UserSettingsPane() {
super("User");
setSpacing(10);
// Display of profile pic change mechanism
final var hbox = new HBox();
// TODO: display current profile pic
profilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon"));
profilePic.setCursor(Cursor.HAND);
profilePic.setFitWidth(60);
profilePic.setFitHeight(60);
profilePic.setOnMouseClicked(e -> {
if (!client.isOnline()) return;
final var pictureChooser = new FileChooser();
pictureChooser.setTitle("Select a new profile pic");
pictureChooser.setInitialDirectory(new File(System.getProperty("user.home")));
pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"));
final var file = pictureChooser.showOpenDialog(Context.getInstance().getSceneContext().getStage());
if (file != null) {
// Check max file size
// TODO: Move to config
if (file.length() > 5E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!").showAndWait();
return;
}
try {
currentImageBytes = Files.readAllBytes(file.toPath());
profilePic.setImage(new Image(new ByteArrayInputStream(currentImageBytes)));
profilePicChanged = true;
} catch (final IOException e1) {
e1.printStackTrace();
}
}
});
hbox.getChildren().add(profilePic);
// Displaying the username change mechanism
final var username = client.getSender().getName();
newUsername = username;
usernameTextField.setText(username);
final EventHandler<? super InputEvent> textChanged = e -> {
newUsername = usernameTextField.getText();
usernameChanged = newUsername != username;
};
usernameTextField.setOnInputMethodTextChanged(textChanged);
usernameTextField.setOnKeyTyped(textChanged);
hbox.getChildren().add(usernameTextField);
getChildren().add(hbox);
// "Displaying" the password change mechanism
final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() };
final Label[] passwordLabels = { new Label("Enter current password:"), new Label("Enter new password:"),
new Label("Repeat new password:") };
final PasswordField[] passwordFields = { currentPasswordField, newPasswordField, repeatNewPasswordField };
final EventHandler<? super InputEvent> passwordEntered = e -> {
newPassword = newPasswordField.getText();
validPassword = newPassword.equals(repeatNewPasswordField.getText())
&& !newPasswordField.getText().isBlank();
};
newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered);
repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered);
repeatNewPasswordField.setOnKeyTyped(passwordEntered);
for (int i = 0; i < passwordHBoxes.length; i++) {
final var hBox2 = passwordHBoxes[i];
passwordLabels[i].setWrapText(true);
hBox2.getChildren().add(passwordLabels[i]);
hBox2.getChildren().add(passwordFields[i]);
getChildren().add(hBox2);
}
// Displaying the save button
saveButton.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT);
getChildren().add(saveButton);
}
/**
* Saves the given input and sends the changed input to the server
*
* @param username the new username
* @since Envoy Client v0.2-beta
*/
private void save(long userID, String oldPassword) {
// The profile pic was changed
if (profilePicChanged) {
final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes, userID);
eventBus.dispatch(profilePicChangeEvent);
client.send(profilePicChangeEvent);
logger.log(Level.INFO, "The user just changed his profile pic.");
}
// The username was changed
final var validContactName = Bounds.isValidContactName(newUsername);
if (usernameChanged && validContactName) {
final var nameChangeEvent = new NameChange(userID, newUsername);
eventBus.dispatch(nameChangeEvent);
client.send(nameChangeEvent);
logger.log(Level.INFO, "The user just changed his name to " + newUsername + ".");
} else if (!validContactName) {
final var alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid username");
alert.setContentText("The entered username does not conform with the naming limitations: " + Bounds.CONTACT_NAME_PATTERN);
alert.showAndWait();
logger.log(Level.INFO, "An invalid username was requested.");
return;
}
// The password was changed
if (validPassword) {
client.send(new PasswordChangeRequest(newPassword, oldPassword, userID));
logger.log(Level.INFO, "The user just tried to change his password!");
} else if (!(validPassword || newPassword.isBlank())) {
final var alert = new Alert(AlertType.ERROR);
alert.setTitle("Unequal Password");
alert.setContentText("Repeated password is unequal to the chosen new password");
alert.showAndWait();
}
}
}

View File

@ -1,10 +1,6 @@
/** /**
* This package contains classes used for representing the settings * This package contains classes used for representing the settings
* visually. * visually.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>19 Apr 2020</strong><br>
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart

View File

@ -1,9 +1,12 @@
package envoy.client.ui; package envoy.client.util;
import java.util.EnumMap; import java.awt.image.BufferedImage;
import java.util.EnumSet; import java.io.IOException;
import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import javax.imageio.ImageIO;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import envoy.client.data.Settings; import envoy.client.data.Settings;
@ -12,15 +15,11 @@ import envoy.util.EnvoyLog;
/** /**
* Provides static utility methods for loading icons from the resource * Provides static utility methods for loading icons from the resource
* folder. * folder.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>IconUtil.java</strong><br>
* Created: <strong>16.03.2020</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class IconUtil { public final class IconUtil {
private IconUtil() {} private IconUtil() {}
@ -31,15 +30,7 @@ public class IconUtil {
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path) { public static Image load(String path) { return new Image(IconUtil.class.getResource(path).toExternalForm()); }
Image image = null;
try {
image = new Image(IconUtil.class.getResource(path).toExternalForm());
} catch (final NullPointerException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
}
return image;
}
/** /**
* Loads an image from the resource folder and scales it to the given size. * Loads an image from the resource folder and scales it to the given size.
@ -50,13 +41,7 @@ public class IconUtil {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path, int size) { public static Image load(String path, int size) {
Image image = null; return new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
try {
image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
} catch (final NullPointerException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
}
return image;
} }
/** /**
@ -145,6 +130,23 @@ public class IconUtil {
return icons; return icons;
} }
/**
* Loads a buffered image from the resource folder which is compatible with AWT.
*
* @param path the path to the icon inside the resource folder
* @return the loaded image
* @since Envoy Client v0.2-beta
*/
public static BufferedImage loadAWTCompatible(String path) {
BufferedImage image = null;
try {
image = ImageIO.read(IconUtil.class.getResource(path));
} catch (final IOException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
}
return image;
}
/** /**
* This method should be called if the display of an image depends upon the * This method should be called if the display of an image depends upon the
* currently active theme.<br> * currently active theme.<br>
@ -154,7 +156,7 @@ public class IconUtil {
* @return the theme specific folder * @return the theme specific folder
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static String themeSpecificSubFolder() { private static String themeSpecificSubFolder() {
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : ""; return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
} }
} }

View File

@ -0,0 +1,80 @@
package envoy.client.util;
import java.util.*;
import java.util.stream.*;
import javafx.scene.Node;
/**
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class ReflectionUtil {
private ReflectionUtil() {}
/**
* Gets all declared variable values of the given instance that have the
* specified class.
* <p>
* (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a
* GUI class).
* <p>
* <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
*
* @param <T> the type of the object
* @param <R> the type to return
* @param instance the instance of a given class whose values are to be
* evaluated
* @param typeToReturn the type of variable to return
* @return all variables in the given instance that have the requested type
* @throws RuntimeException if an exception occurs
* @since Envoy Client v0.2-beta
*/
public static <T, R> Stream<R> getAllDeclaredVariablesOfTypeAsStream(T instance, Class<R> typeToReturn) {
return Arrays.stream(instance.getClass().getDeclaredFields()).filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> {
try {
field.setAccessible(true);
return typeToReturn.cast(field.get(instance));
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
/**
* Gets all declared variables of the given instance that are children of
* {@code Node}.
* <p>
* <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
*
* @param <T> the type of the instance
* @param instance the instance of a given class whose values are to be
* evaluated
* @return all variables of the given object that have the requested type as
* {@code Stream}
* @since Envoy Client v0.2-beta
*/
public static <T> Stream<Node> getAllDeclaredNodeVariablesAsStream(T instance) {
return getAllDeclaredVariablesOfTypeAsStream(instance, Node.class);
}
/**
* Gets all declared variables of the given instance that are children of
* {@code Node}<br>
* <p>
* <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
*
* @param <T> the type of the instance
* @param instance the instance of a given class whose values are to be
* evaluated
* @return all variables of the given object that have the requested type
* @since Envoy Client v0.2-beta
*/
public static <T> List<Node> getAllDeclaredNodeVariables(T instance) {
return getAllDeclaredNodeVariablesAsStream(instance).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,7 @@
/**
* This package contains utility classes for use in envoy-client.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
package envoy.client.util;

View File

@ -7,17 +7,21 @@
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
module envoy { module envoy.client {
requires transitive envoy.common; requires envoy.common;
requires transitive java.desktop; requires java.desktop;
requires transitive java.logging; requires java.logging;
requires transitive java.prefs; requires java.prefs;
requires javafx.controls; requires javafx.controls;
requires javafx.fxml; requires javafx.fxml;
requires javafx.base; requires javafx.base;
requires javafx.graphics; requires javafx.graphics;
opens envoy.client.ui to javafx.graphics, javafx.fxml; opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus;
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml; opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
opens envoy.client.ui.control to javafx.graphics, javafx.fxml;
opens envoy.client.ui.settings to envoy.client.util;
opens envoy.client.net to dev.kske.eventbus;
opens envoy.client.data to dev.kske.eventbus;
} }

View File

@ -1,3 +1,6 @@
server=localhost server=localhost
port=8080 port=8080
localDB=.\\localDB localDB=localDB
localDBSaveInterval=2
consoleLevelBarrier=FINER
fileLevelBarrier=OFF

View File

@ -1,5 +1,5 @@
.button, .list-cell, .progress-bar * { .button, .list-cell, .progress-bar * {
-fx-background-radius: 5.0em; -fx-background-radius: 0.3em;
} }
.context-menu, .context-menu > * { .context-menu, .context-menu > * {
@ -8,6 +8,28 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
#text-enter-container, #contact-search-enter-container {
-fx-background-radius: 5.0em;
}
#round-button {
-fx-background-radius: 5.0em;
}
.text-area {
-fx-background-color: transparent;
}
.text-area .scroll-pane {
-fx-background-color: transparent;
}
.text-area .scroll-pane .viewport{
-fx-background-color: transparent;
}
.text-area .scroll-pane .content{
-fx-background-color: transparent;
}
.menu-item { .menu-item {
-fx-background-radius: 15.0px; -fx-background-radius: 15.0px;
} }
@ -48,40 +70,75 @@
.received-message { .received-message {
-fx-alignment: center-left; -fx-alignment: center-left;
-fx-background-radius: 4.0em; -fx-background-radius: 1.3em;
-fx-text-alignment: right; -fx-text-alignment: right;
} }
.own-message { .own-message {
-fx-alignment: center-right; -fx-alignment: center-right;
-fx-background-radius: 4.0em; -fx-background-radius: 1.3em;
-fx-text-alignment: left; -fx-text-alignment: left;
} }
.unreadMessagesAmount { .unread-messages-amount {
-fx-alignment: center; -fx-alignment: center;
-fx-background-color: orange; -fx-background-color: orange;
-fx-background-radius: 4.0em; -fx-background-radius: 4.0em;
-fx-text-alignment: center; -fx-text-alignment: center;
} }
#remainingCharsLabel { #login-button {
-fx-background-radius: 1.0em;
}
#register-switch {
-fx-background-color: transparent;
-fx-text-fill: orange;
}
#login-input-field {
-fx-background-color: transparent;
-fx-border: solid;
-fx-border-width: 0.0 0.0 1.0 0.0;
}
#remaining-chars-label {
-fx-text-fill: #00FF00; -fx-text-fill: #00FF00;
-fx-background-color: transparent; -fx-background-color: transparent;
} }
#infoLabel-success { #info-label-success {
-fx-text-fill: #00FF00; -fx-text-fill: #00FF00;
} }
#infoLabel-info { #info-label-info {
-fx-text-fill: yellow; -fx-text-fill: yellow;
} }
#infoLabel-warning { #info-label-warning {
-fx-text-fill: orange; -fx-text-fill: orange;
} }
#infoLabel-error { #info-label-error {
-fx-text-fill: red; -fx-text-fill: red;
} }
#transparent-background {
-fx-background-color: transparent;
}
#profile-pic {
-fx-radius: 1.0em;
}
.list-element {
-fx-background-color: transparent;
}
.tab-pane {
-fx-tab-max-height: 0.0 ;
}
.tab-pane .tab-header-area {
visibility: hidden ;
-fx-padding: -20.0 0.0 0.0 0.0;
}

View File

@ -7,7 +7,7 @@
} }
.button { .button {
-fx-background-color: rgb(105.0,0.0,153.0); -fx-background-color: #690099;
} }
.button:pressed { .button:pressed {
@ -18,12 +18,12 @@
-fx-background-color: lightgray; -fx-background-color: lightgray;
} }
.list-view, .list-cell, .text-area .content, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item { #message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item {
-fx-background-color: dimgray; -fx-background-color: #222222;
} }
.list-cell:selected, .list-cell:selected > *, .menu-item:hover { .list-cell:selected, .list-cell:selected > *, .menu-item:hover {
-fx-background-color: rgb(105.0,0.0,153.0); -fx-background-color: #690099;
} }
.received-message { .received-message {
@ -37,3 +37,53 @@
.alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane { .alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane {
-fx-background-color: black; -fx-background-color: black;
} }
#login-input-field {
-fx-border-color: white;
}
#login-background {
-fx-background-color: #191919;
}
#chat-list, #top-bar, #search-panel, #note-background {
-fx-background-color: #303030;
}
#text-enter-container {
-fx-background-color: #363636;
}
#contact-search-enter-container {
-fx-background-color: #202020;
}
#underline {
-fx-border: solid;
-fx-border-width: 0.0 0.0 1.0 0.0;
-fx-border-color: #202020;
}
.group-member-names {
-fx-text-fill: #690099;
-fx-font-weight: bold;
}
.scroll-bar:vertical, .scroll-bar:vertical .track, .scroll-bar:vertical .increment-button , .scroll-bar:vertical .decrement-button {
-fx-background-color: transparent;
}
.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow {
-fx-background-color: transparent;
}
.scroll-bar:vertical .thumb {
-fx-background-color: #707070;
-fx-background-insets : 4.0, 0.0, 0.0;
-fx-background-radius : 2.0em;
}
#proceed-button {
-fx-text-fill: white;
-fx-background-color: transparent;
}

View File

@ -1,16 +1,85 @@
.button{ .root {
-fx-background-color: orangered; -fx-background-color: #E6E6E6;
} }
.list-cell:selected, .list-cell:selected > * { .button {
-fx-background-color: orangered; -fx-background-color: #B37D7D;
-fx-text-fill: black;
} }
.received-message, .menu-item { .button:pressed {
-fx-background-color: #808080;
}
.button:disabled {
-fx-background-color: lightgray;
}
#message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item {
-fx-background-color: #E3E3E3;
}
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
-fx-background-color: #805959;
}
.received-message {
-fx-background-color: lightgray; -fx-background-color: lightgray;
} }
.own-message { .own-message {
-fx-background-color: lightgreen; -fx-background-color: #8FA88F;
}
.alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane {
-fx-background-color: black;
}
#login-input-field {
-fx-border-color: black;
}
#login-background {
-fx-background-color: white;
}
#chat-list, #top-bar, #search-panel, #note-background {
-fx-background-color: white;
}
#text-enter-container {
-fx-background-color: #F2F2F2;
}
#contact-search-enter-container {
-fx-background-color: #CCCCCC;
}
#underline {
-fx-border: solid;
-fx-border-width: 0.0 0.0 1.0 0.0;
-fx-border-color: #CCCCCC;
}
.group-member-names {
-fx-text-fill: #805959;
-fx-font-weight: bold;
}
.scroll-bar:vertical, .scroll-bar:vertical .track, .scroll-bar:vertical .increment-button , .scroll-bar:vertical .decrement-button {
-fx-background-color: transparent;
}
.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow {
-fx-background-color: transparent;
}
.scroll-bar:vertical .thumb {
-fx-background-color: #707070;
-fx-background-insets : 4.0, 0.0, 0.0;
-fx-background-radius : 2.0em;
}
#proceed-button {
-fx-text-fill: white;
-fx-background-color: transparent;
} }

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