283 Commits

Author SHA1 Message Date
6b02fd0f46 Add Datamodel 2020-10-24 12:39:51 +02:00
a062911d55 Handshake Sequence UML 2020-10-24 12:37:33 +02:00
241e5def03 Fix "infinite" amount of messages being displayed
Fixes #105
2020-10-22 16:15:34 +02:00
cd8971b6b4 Fix settings button placement
Fixes #94
2020-10-22 16:09:09 +02:00
e79f60e95e Properly display unread message count (>9) 2020-10-22 15:32:06 +02:00
aaaf5ef7be Fix TrayIcon colors 2020-10-22 12:17:44 +02:00
98f59c1383 Fix bug displaying the double amount of unread messages
Additionally remove ChangeHandlers in SettingsItem and show
StatusTrayIcon whenever supported
2020-10-21 22:14:04 +02:00
db28f02505 Merge branch 'develop' into f/enhanced-status-tray-icon
Conflicts:
	client/src/main/java/envoy/client/data/Chat.java
	client/src/main/java/envoy/client/ui/StatusTrayIcon.java
2020-10-19 18:36:07 +02:00
b2c3cf62c8 Reformat all source files with new formatter 2020-10-19 18:17:51 +02:00
77a75fc37c Apply suggestions by @kske
Additionally fixed three bugs/ inconsistencies:
- status changing keyboard shortcuts are present again
- deleted cells no longer show a hand cursor
- any event method in LocalDB needing higher priority now has priority
500
2020-10-19 18:09:19 +02:00
a0812f193e Add working leaving of a group
Additionally fixed a two bugs:
- one group member will no longer show "1 members"
- deletion of empty groups no longer throws an exception
2020-10-19 18:09:19 +02:00
ebe19c00c9 Move context menu from ChatScene globally to ChatControl specific
Additionally fixed a small bug in UserCreationProcessor and when
deleting a contact offline
2020-10-19 18:09:19 +02:00
dd477b6cbc Fix several bugs
These are:
- not immediately updating ChatScene if a contact was blocked and both
are online
- ContactSearchTab showing previously entered text on reopening (without
showing suggestions)
- users not getting notified if someone else blocked them while they
were offline
- no logger statements in UserUtil

Still to do:
- Move ContextMenu from chatList to ChatControl
- Test behavior for groups
2020-10-19 18:09:19 +02:00
571a953c40 Add partially working blocking and deletion (for both client and server)
Additionally had to refactor several classes "a little bit".
(Whenever one bug seemed fixed, another one appeared...)
2020-10-19 18:09:19 +02:00
a515ec961a Add server side contact deletion 2020-10-19 18:09:19 +02:00
12184848b6 Remove unused value from client.properties
The configuration values are now ordered as describes in the Wiki
article at https://git.kske.dev/zdm/envoy/wiki/Client-Configuration
2020-10-19 08:53:50 +02:00
2e17caea4d Keep track of total unread messages and display them in the status tray 2020-10-18 16:45:36 +02:00
44f4d8f1e0 Display current user status in status tray icon 2020-10-18 15:54:44 +02:00
5b85c1bf54 Remove project specific import order 2020-10-18 12:13:47 +02:00
f4f34ff829 Remove project specific formatters 2020-10-18 12:09:58 +02:00
ab2e9aa114 Add Image Caching (#95)
Add image caching

Co-authored-by: kske <kai@kske.dev>
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/95
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-13 11:30:19 +02:00
75f0a65517 Add Enhanced Keyboard Shortcut Mechanism (#91)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/91
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: kske <kai@kske.dev>
2020-10-12 16:12:23 +02:00
08bd915f04 Improve performance of a status update
Additionally fixed a bug causing unnecessary warnings on the server
2020-10-10 12:56:16 +02:00
fa2a5d0b24 Fix Bug resetting user status on login 2020-10-09 18:23:00 +02:00
1d191858fe Apply suggestions by @kske 2020-10-09 12:03:28 +02:00
3c8c544cbd Added shortcuts to change status and TrayIconPopupMenu items
Fixes #14
2020-10-08 17:09:09 +02:00
e8202e0c94 Added display of your own status
Fixes #85
2020-10-08 17:03:14 +02:00
3810fdef02 Fixed a bug showing the wrong user status in ChatScene top bar
Additionally refactored UI components a bit
2020-10-08 14:34:21 +02:00
637ad9f61f Added ability to change user status 2020-10-07 23:43:30 +02:00
f2eb89d469 Make PersistenceManager Less Error Prone (#83)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/83
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-07 22:13:42 +02:00
6f9982bbc3 Notify user about unsuccessful system command execution
Additionally added error system command.
Fixes #75
2020-10-07 21:48:11 +02:00
5e1b9a9d5b Offer customization of activator char in SystemCommandMap
Additionally cleaned up SystemCommandMap a lot.
This commit will also be the foundation of Envoy CLI, as it enables no
activator as well.
2020-10-06 22:00:55 +02:00
fb1147f939 Add Local Deletion of Messages Also for Messages You did not Send (#80)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/80
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-05 18:52:05 +02:00
7ca770cbc3 Fix Incorrect ChatScene Size on Startup (#79)
Additionally fixes error on message receival.
Fixes #68
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/79
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-05 18:50:45 +02:00
da6bdafc68 Fix Bug Showing Incorrect User Statuses (#78)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/78
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-04 21:53:58 +02:00
99867eb23a Merge pull request 'Quick Select Support for the GroupCreationTab' (#77) from f/quick-group-select into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/77
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: delvh <leon@kske.dev> (With a little discussion topic :))
2020-10-04 21:28:55 +02:00
994cbbcd72 Implemented change requests by @delvh and @kske 2020-10-04 17:11:45 +02:00
51b189e8f5 Implemented some requested changes 2020-10-03 20:28:43 +02:00
3d987985ff Added javadoc 2020-10-03 15:19:37 +02:00
5f0910635a Added insets 2020-10-03 15:11:38 +02:00
ab77c98a36 Added functionality 2020-10-03 15:08:02 +02:00
434d577c15 Worked on displaying the quickSelect correctly 2020-10-03 14:47:50 +02:00
8c0add517a Fix Typo in README (#76)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/76
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-03 14:24:03 +02:00
9934eefd41 Move SystemComandMap From ChatScene to Its Own Component (#74)
Move SystemComandMap from ChatScene to its own component.
Create message specific commands with their own parser.
Fix separators not shown correctly in TextInputContextMenu.
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/74
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-02 15:23:21 +02:00
8543e94040 Added ability to remove users from quick select list 2020-10-01 22:59:07 +02:00
8592839156 Worked on quickMessageList and corresponding control 2020-10-01 22:29:40 +02:00
7fffa0da83 implemented basic architecture 2020-09-30 21:44:02 +02:00
85d0aa37d2 Adjust Selection Color (#73)
Replace selection color by one unified gray
Center message controls vertically inside their list cells

Fixes #62

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/73
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2020-09-30 21:05:32 +02:00
80795a3fc2 Add Ability to Delete Messages Locally (#70)
Merge branch 'develop' into f/delete-messages
Additionally added system commands to copy, delete or save attachments of selected messages

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/70
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-09-30 20:50:58 +02:00
f5bfb73abe Add System Requirements to README (#71)
Add link to Noto emoji in README

Add system requirements to README

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/71
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-09-30 18:41:53 +02:00
2779971e99 Bump JavaFX Version to 15 to Support Emoji Fonts (#72)
Bump JavaFX version to 15 to support emoji fonts
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/72
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-09-30 18:41:31 +02:00
a4e9474b97 Fixed Saving When Using Alt F4 and Disabled Hiding If StatusTrayIcon is not supported(#65)
Fixed potentially not saving when using alt f4 and disabled hiding if
StatusTrayIcon is not supported
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/65
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-09-28 15:58:42 +02:00
3f0267624c Bumped Version References One Version Up (v0.3-beta) (#64)
Additionally removed <Project:File:Date:> headers from javadoc suggestions

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/64
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: kske <kai@kske.dev>
2020-09-27 17:39:28 +02:00
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
232 changed files with 9467 additions and 5674 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/
# 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

69
README.md Normal file
View File

@ -0,0 +1,69 @@
# 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.
### System requirements
To run Envoy, you have to install a Java Runtime Environment (JRE) of at least version 11.
You can download an open source implementation from [here](https://jdk.java.net/15/).
If you are running a Linux distribution, make sure that an emoji font like [Noto emoji](https://github.com/googlefonts/noto-emoji) is installed.
Most major Linux distributions like Debian, Arch and Gentoo have a Noto emoji package available inside their package repositories.
## 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.
### System requirements
To run Envoy server, you have to install a JRE as mentioned above, as well as a database.
In development, PostgreSQL is used, which you can download from [here](https://www.postgresql.org/download/).
Look at the file `META-INF/persistence.xml` inside `envoy-server.jar` for the database configuration.
After creating a database and configuring the credentials, the server will initialize the necessary tables automatically.
## 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
* Database entities
* Utility classes to check client version compatability and password validity

View File

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

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"?>
<!DOCTYPE xml>
<projectDescription>
<name>envoy-client</name>
<name>client</name>
<comment></comment>
<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.doc.comment.support=enabled
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.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
@ -128,364 +129,4 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=11
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=true
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=1
org.eclipse.jdt.core.formatter.align_type_members_on_columns=true
org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=true
org.eclipse.jdt.core.formatter.align_with_spaces=false
org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=84
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=80
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=20
org.eclipse.jdt.core.formatter.alignment_for_assignment=0
org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16
org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0
org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0
org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16
org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
org.eclipse.jdt.core.formatter.alignment_for_module_statements=16
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16
org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0
org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=84
org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0
org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16
org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0
org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0
org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0
org.eclipse.jdt.core.formatter.blank_lines_after_package=1
org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1
org.eclipse.jdt.core.formatter.blank_lines_before_field=0
org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1
org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
org.eclipse.jdt.core.formatter.blank_lines_before_method=1
org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
org.eclipse.jdt.core.formatter.blank_lines_before_package=0
org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0
org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=true
org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=true
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=true
org.eclipse.jdt.core.formatter.comment.format_block_comments=true
org.eclipse.jdt.core.formatter.comment.format_header=true
org.eclipse.jdt.core.formatter.comment.format_html=true
org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
org.eclipse.jdt.core.formatter.comment.format_line_comments=true
org.eclipse.jdt.core.formatter.comment.format_source_code=true
org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
org.eclipse.jdt.core.formatter.comment.indent_tag_description=false
org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert
org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
org.eclipse.jdt.core.formatter.comment.line_length=80
org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
org.eclipse.jdt.core.formatter.compact_else_if=true
org.eclipse.jdt.core.formatter.continuation_indentation=2
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=true
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_empty_lines=false
org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
org.eclipse.jdt.core.formatter.indentation.size=4
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert
org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert
org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert
org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert
org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.join_lines_in_comments=false
org.eclipse.jdt.core.formatter.join_wrapped_lines=true
org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_if_single_item
org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never
org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=true
org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_if_empty
org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_single_item
org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always
org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_if_empty
org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_if_single_item
org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false
org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=true
org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=true
org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_if_empty
org.eclipse.jdt.core.formatter.lineSplit=150
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0
org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0
org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=separate_lines_if_wrapped
org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines
org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
org.eclipse.jdt.core.formatter.tabulation.char=tab
org.eclipse.jdt.core.formatter.tabulation.size=4
org.eclipse.jdt.core.formatter.text_block_indentation=0
org.eclipse.jdt.core.formatter.use_on_off_tags=false
org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true
org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false
org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true
org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true
org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true
org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true
org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true
org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true
org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter

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,24 +9,24 @@
<parent>
<groupId>informatik-ag-ngl</groupId>
<artifactId>envoy</artifactId>
<version>0.1-beta</version>
<version>0.2-beta</version>
</parent>
<dependencies>
<dependency>
<groupId>informatik-ag-ngl</groupId>
<artifactId>envoy-common</artifactId>
<version>0.1-beta</version>
<version>0.2-beta</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>11.0.2</version>
<version>15</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>11.0.2</version>
<version>15</version>
</dependency>
</dependencies>
@ -37,15 +37,6 @@
<directory>src/main/resources</directory>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -7,24 +7,33 @@ import envoy.client.ui.Startup;
/**
* Triggers application startup.
* <p>
* To allow Maven shading, the main method has to be separated from the
* {@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>
* To allow Maven shading, the main method has to be separated from the {@link Startup} class which
* extends {@link Application}.
*
* @author Kai S. K. Engelbart
* @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.
*
* @param args the command line arguments are processed by the
* client configuration
* @param args the command line arguments are processed by the client configuration
* @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;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.Queue;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.*;
import envoy.util.EnvoyLog;
/**
* 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
* @author Kai S. K. Engelbart
@ -41,7 +35,9 @@ public final class Cache<T> implements Consumer<T>, Serializable {
}
@Override
public String toString() { return String.format("Cache[elements=" + elements + "]"); }
public String toString() {
return String.format("Cache[elements=" + elements + "]");
}
/**
* Sets the processor to which cached elements are relayed.
@ -58,8 +54,18 @@ public final class Cache<T> implements Consumer<T>, Serializable {
* @since Envoy Client v0.3-alpha
*/
public void relay() {
if (processor == null) throw new IllegalStateException("Processor is not defined");
if (processor == null)
throw new IllegalStateException("Processor is not defined");
elements.forEach(processor::accept);
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,11 @@
package envoy.client.data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
/**
* Stores a heterogeneous map of {@link Cache} objects with different type
* parameters.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>CacheMap.java</strong><br>
* Created: <strong>09.07.2020</strong><br>
*
* Stores a heterogeneous map of {@link Cache} objects with different type parameters.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
@ -23,27 +17,31 @@ public final class CacheMap implements Serializable {
/**
* Adds a cache to the map.
*
*
* @param <T> the type accepted by the cache
* @param key the class that maps to the cache
* @param cache the cache to store
* @since Envoy Client v0.1-beta
*/
public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); }
public <T> void put(Class<T> key, Cache<T> cache) {
map.put(key, cache);
}
/**
* Returns a cache mapped by a class.
*
*
* @param <T> the type accepted by the cache
* @param key the class that maps to the cache
* @return the cache
* @since Envoy Client v0.1-beta
*/
public <T> Cache<T> get(Class<T> key) { return (Cache<T>) map.get(key); }
public <T> Cache<T> get(Class<T> key) {
return (Cache<T>) map.get(key);
}
/**
* Returns a cache mapped by a class or any of its subclasses.
*
*
* @param <T> the type accepted by the cache
* @param key the class that maps to the cache
* @return the cache
@ -52,7 +50,7 @@ public final class CacheMap implements Serializable {
public <T> Cache<? super T> getApplicable(Class<T> key) {
Cache<? super T> cache = get(key);
if (cache == null)
for (var e : map.entrySet())
for (final var e : map.entrySet())
if (e.getKey().isAssignableFrom(key))
cache = (Cache<? super T>) e.getValue();
return cache;
@ -63,4 +61,13 @@ public final class CacheMap implements Serializable {
* @since Envoy Client v0.1-beta
*/
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,23 +1,19 @@
package envoy.client.data;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.io.*;
import java.util.*;
import javafx.beans.property.*;
import javafx.collections.*;
import envoy.client.net.WriteProxy;
import envoy.data.*;
import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange;
import envoy.client.net.WriteProxy;
/**
* Represents a chat between two {@link User}s
* 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>
* Represents a chat between two {@link User}s as a list of {@link Message} objects.
*
* @author Maximilian K&auml;fer
* @author Leon Hofmeister
@ -26,12 +22,21 @@ import envoy.event.MessageStatusChange;
*/
public class Chat implements Serializable {
protected final Contact recipient;
protected final List<Message> messages = new ArrayList<>();
protected boolean disabled;
protected int unreadAmount;
/**
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
*/
protected transient long lastWritingEvent;
private static final long serialVersionUID = 1L;
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount;
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
protected final Contact recipient;
private static final long serialVersionUID = 2L;
/**
* Provides the list of messages that the recipient receives.
@ -42,66 +47,91 @@ public class Chat implements Serializable {
* @since Envoy Client v0.1-alpha
*/
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());
totalUnreadAmount.set(totalUnreadAmount.get() + unreadAmount);
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeObject(new ArrayList<>(messages));
}
@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,disabled=%b]",
getClass().getSimpleName(),
recipient,
messages.size(),
disabled);
}
/**
* Generates a hash code based on the recipient.
*
*
* @since Envoy Client v0.1-beta
*/
@Override
public int hashCode() { return Objects.hash(recipient); }
public int hashCode() {
return Objects.hash(recipient);
}
/**
* Tests equality to another object based on the recipient.
*
*
* @since Envoy Client v0.1-beta
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Chat)) return false;
Chat other = (Chat) obj;
if (this == obj)
return true;
if (!(obj instanceof Chat))
return false;
final var other = (Chat) obj;
return Objects.equals(recipient, other.recipient);
}
/**
* Sets the status of all chat messages received from the recipient to
* {@code READ} starting from the bottom and stopping once a read message is
* found.
* Sets the status of all chat messages received from the recipient to {@code READ} starting
* from the bottom and stopping once a read message is found.
*
* @param writeProxy the write proxy instance used to notify the server about
* the message status changes
* @throws IOException if a {@link MessageStatusChange} could not be
* delivered to the server
* @param writeProxy the write proxy instance used to notify the server about the message status
* changes
* @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) {
final Message m = messages.get(i);
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
else {
m.setStatus(MessageStatus.READ);
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
}
final var m = messages.get(i);
if (m.getSenderID() == recipient.getID())
if (m.getStatus() == MessageStatus.READ)
break;
else {
m.setStatus(MessageStatus.READ);
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
}
}
totalUnreadAmount.set(totalUnreadAmount.get() - unreadAmount);
unreadAmount = 0;
}
/**
* @return {@code true} if the newest message received in the chat doesn't have
* the status {@code READ}
* @return {@code true} if the newest message received in the chat doesn't have the status
* {@code READ}
* @since Envoy Client v0.3-alpha
*/
public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
public boolean isUnread() {
return !messages.isEmpty()
&& messages.get(messages.size() - 1).getStatus() != MessageStatus.READ;
}
/**
* Inserts a message at the correct place according to its creation date.
*
*
* @param message the message to insert
* @since Envoy Client v0.1-beta
*/
@ -115,14 +145,34 @@ public class Chat implements Serializable {
}
/**
* Increments the amount of unread messages.
*
* @since Envoy Client v0.1-beta
* Removes the message with the given ID.
*
* @param messageID the ID of the message to remove
* @return whether the message has been found and removed
* @since Envoy Client v0.3-beta
*/
public void incrementUnreadAmount() { unreadAmount++; }
public boolean remove(long messageID) {
return messages.removeIf(m -> m.getID() == messageID);
}
/**
* @return the amount of unread mesages in this chat
* @return an integer property storing the total amount of unread messages
* @since Envoy Client v0.3-beta
*/
public static IntegerProperty getTotalUnreadAmount() { return totalUnreadAmount; }
/**
* Increments the amount of unread messages.
*
* @since Envoy Client v0.1-beta
*/
public void incrementUnreadAmount() {
++unreadAmount;
totalUnreadAmount.set(totalUnreadAmount.get() + 1);
}
/**
* @return the amount of unread messages in this chat
* @since Envoy Client v0.1-beta
*/
public int getUnreadAmount() { return unreadAmount; }
@ -131,7 +181,7 @@ public class Chat implements Serializable {
* @return all messages in the current chat
* @since Envoy Client v0.1-beta
*/
public List<Message> getMessages() { return messages; }
public ObservableList<Message> getMessages() { return messages; }
/**
* @return the recipient of a message
@ -140,14 +190,35 @@ public class Chat implements Serializable {
public Contact getRecipient() { return recipient; }
/**
* @return whether this {@link Chat} points at a {@link User}
* @since Envoy Client v0.1-beta
* @return the last known time a {@link envoy.event.IsTyping} event has been 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}
* @since Envoy Client v0.1-beta
* Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}.
*
* @since Envoy Client v0.2-beta
*/
public boolean isGroupChat() { return recipient instanceof Group; }
public void lastWritingEventWasNow() {
lastWritingEvent = System.currentTimeMillis();
}
/**
* Determines whether messages can be sent in this chat. Should be {@code true} i.e. for chats
* whose recipient deleted this client as a contact.
*
* @return whether this chat has been disabled
* @since Envoy Client v0.3-beta
*/
public boolean isDisabled() { return disabled; }
/**
* Determines whether messages can be sent in this chat. Should be true i.e. for chats whose
* recipient deleted this client as a contact.
*
* @param disabled whether this chat should be disabled
* @since Envoy Client v0.3-beta
*/
public void setDisabled(boolean disabled) { this.disabled = disabled; }
}

View File

@ -2,26 +2,16 @@ package envoy.client.data;
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.ConfigItem;
import envoy.data.LoginCredentials;
/**
* Implements a configuration specific to the Envoy Client with default values
* and convenience methods.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ClientConfig.java</strong><br>
* Created: <strong>01.03.2020</strong><br>
* Implements a configuration specific to the Envoy Client with default values and convenience
* methods.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class ClientConfig extends Config {
public final class ClientConfig extends Config {
private static ClientConfig config;
@ -30,20 +20,16 @@ public class ClientConfig extends Config {
* @since Envoy Client v0.1-beta
*/
public static ClientConfig getInstance() {
if (config == null) config = new ClientConfig();
if (config == null)
config = new ClientConfig();
return config;
}
private ClientConfig() {
items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
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()));
super(".envoy");
put("server", "s", identity());
put("port", "p", Integer::parseInt);
put("localDBSaveInterval", "db-si", Integer::parseInt);
}
/**
@ -59,57 +45,10 @@ public class ClientConfig extends Config {
public Integer getPort() { return (Integer) items.get("port").get(); }
/**
* @return the local database specific to the client user
* @since Envoy Client v0.1-alpha
* @return the amount of minutes after which the local database should be saved
* @since Envoy Client v0.2-beta
*/
public File getLocalDB() { return (File) items.get("localDB").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); }
public Integer getLocalDBSaveInterval() {
return (Integer) items.get("localDBSaveInterval").get();
}
}

View File

@ -0,0 +1,94 @@
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;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.Instant;
import envoy.client.net.WriteProxy;
import envoy.data.Contact;
import envoy.data.GroupMessage;
import envoy.data.*;
import envoy.data.Message.MessageStatus;
import envoy.data.User;
import envoy.event.GroupMessageStatusChange;
import envoy.client.net.WriteProxy;
/**
* Represents a chat between a user and a group
* 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>
*
* Represents a chat between a user and a group as a list of messages.
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupChat extends Chat {
public final class GroupChat extends Chat {
private final User sender;
@ -32,23 +25,23 @@ public class GroupChat extends Chat {
* @param recipient the group whose members receive the messages
* @since Envoy Client v0.1-beta
*/
public GroupChat(User sender, Contact recipient) {
public GroupChat(User sender, Group recipient) {
super(recipient);
this.sender = sender;
}
@Override
public void read(WriteProxy writeProxy) throws IOException {
public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) {
final GroupMessage gmsg = (GroupMessage) messages.get(i);
if (gmsg.getSenderID() != sender.getID()) {
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
if (gmsg.getSenderID() != sender.getID())
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ)
break;
else {
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
writeProxy
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(),
MessageStatus.READ, Instant.now(), sender.getID()));
}
}
}
unreadAmount = 0;
}

View File

@ -1,116 +1,443 @@
package envoy.client.data;
import static java.util.function.Predicate.not;
import java.io.*;
import java.nio.channels.*;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.*;
import java.util.logging.*;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.collections.*;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
import envoy.data.*;
import envoy.event.GroupResize;
import envoy.event.MessageStatusChange;
import envoy.event.NameChange;
import envoy.data.Message.MessageStatus;
import envoy.event.*;
import envoy.event.contact.*;
import envoy.exception.EnvoyException;
import envoy.util.*;
import envoy.client.event.*;
/**
* Stores information about the current {@link User} and their {@link Chat}s.
* For message ID generation a {@link IDGenerator} is stored as well.
* Stores information about the current {@link User} and their {@link Chat}s. For message ID
* generation a {@link IDGenerator} is stored as well.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>LocalDB.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
* The managed objects are stored inside a folder in the local file system.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public abstract class LocalDB {
public final class LocalDB implements EventListener {
protected User user;
protected Map<String, Contact> users = new HashMap<>();
protected List<Chat> chats = new ArrayList<>();
protected IDGenerator idGenerator;
protected CacheMap cacheMap = new CacheMap();
// Data
private User user;
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
private ObservableList<Chat> chats = FXCollections.observableArrayList();
private IDGenerator idGenerator;
private CacheMap cacheMap = new CacheMap();
private String authToken;
private boolean contactsChanged;
{
// 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(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
* as well. The message id generator will also be saved if present.
* Loads the local user registry {@code users.db}, the id generator {@code id_gen.db} and last
* login file {@code last_login.db}.
*
* @throws Exception if the saving process failed
* @since Envoy Client v0.3-alpha
* @since Envoy Client v0.2-beta
*/
public void save() throws Exception {}
/**
* Loads all user data.
*
* @throws Exception if the loading process failed
* @since Envoy Client v0.3-alpha
*/
public void loadUsers() throws Exception {}
private synchronized void loadGlobalData() {
try {
try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
users = (Map<String, User>) in.readObject();
}
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
user = (User) in.readObject();
authToken = (String) in.readObject();
}
} catch (IOException | ClassNotFoundException e) {}
}
/**
* 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
*/
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");
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
Chat.getTotalUnreadAmount().set(0);
chats = FXCollections.observableList((List<Chat>) in.readObject());
// Some chats have changed and should not be overwritten by the saved values
if (contactsChanged) {
final var contacts = user.getContacts();
// Mark chats as disabled if a contact is no longer in this users contact list
final var changedUserChats = chats.stream()
.filter(not(chat -> contacts.contains(chat.getRecipient())))
.peek(chat -> {
chat.setDisabled(true);
logger.log(Level.INFO,
String.format("Deleted chat with %s.", chat.getRecipient()));
});
// Also update groups with a different member count
final var changedGroupChats =
contacts.stream().filter(Group.class::isInstance).flatMap(group -> {
final var potentialChat = getChat(group.getID());
if (potentialChat.isEmpty())
return Stream.empty();
final var chat = potentialChat.get();
if (group.getContacts().size() != chat.getRecipient().getContacts()
.size()) {
logger.log(Level.INFO, "Removed one (or more) members from " + group);
return Stream.of(chat);
} else
return Stream.empty();
});
Stream.concat(changedUserChats, changedGroupChats)
.forEach(chat -> chats.set(chats.indexOf(chat), chat));
// loadUserData can get called two (or more?) times during application lifecycle
contactsChanged = false;
}
cacheMap = (CacheMap) in.readObject();
lastSync = (Instant) in.readObject();
} finally {
synchronize();
}
}
/**
* Loads the ID generator. Any exception thrown during this process is ignored.
* Synchronizes the contact list of the client user with the chat and user storage.
*
* @since Envoy Client v0.3-alpha
*/
public void loadIDGenerator() {}
/**
* Synchronizes the contact list of the client user with the chat and user
* storage.
*
* @since Envoy Client v0.1-beta
*/
public void synchronize() {
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
private void synchronize() {
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);
// Synchronize user status data
for (Contact contact : users.values())
for (final var contact : user.getContacts())
if (contact instanceof User)
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
getChat(contact.getID()).ifPresent(chat -> {
((User) chat.getRecipient()).setStatus(((User) contact).getStatus());
});
// Create missing chats
user.getContacts()
.stream()
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, (Group) c))
.forEach(chats::add);
}
/**
* @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys
* 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 = 500)
private synchronized void save() {
EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "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 = 500)
private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT)
msg.nextStatus();
}
@Event(priority = 500)
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 = 500)
private void onMessageStatusChange(MessageStatusChange evt) {
getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get()));
}
@Event(priority = 500)
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
this.<GroupMessage>getMessage(evt.getID())
.ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
}
@Event(priority = 500)
private void onUserStatusChange(UserStatusChange evt) {
getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast)
.ifPresent(u -> u.setStatus(evt.get()));
}
@Event(priority = 500)
private void onUserOperation(UserOperation operation) {
final var eventUser = operation.get();
switch (operation.getOperationType()) {
case ADD:
Platform.runLater(() -> chats.add(0, new Chat(eventUser)));
break;
case REMOVE:
getChat(eventUser.getID()).ifPresent(chat -> chat.setDisabled(true));
break;
}
}
@Event
private void onGroupCreationResult(GroupCreationResult evt) {
final var newGroup = evt.get();
// The group creation was not successful
if (newGroup == null)
return;
// The group was successfully created
else
Platform.runLater(() -> chats.add(new GroupChat(user, newGroup)));
}
@Event(priority = 500)
private void onGroupResize(GroupResize evt) {
getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast)
.ifPresent(evt::apply);
}
@Event(priority = 500)
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 = 50)
private void onLogout() {
autoSaver.cancel();
autoSaveRestart = true;
lastLoginFile.delete();
userFile = null;
user = null;
authToken = null;
chats.clear();
lastSync = Instant.EPOCH;
cacheMap.clear();
}
/**
* Deletes the message with the given ID, if present.
*
* @param message the event that was
* @since Envoy Client v0.3-beta
*/
@Event
private void onMessageDeletion(MessageDeletion message) {
Platform.runLater(() -> {
// We suppose that messages have unique IDs, hence the search can be stopped
// once a message was removed
final var messageID = message.get();
for (final var chat : chats)
if (chat.remove(messageID))
break;
});
}
@Event(priority = 500)
private void onOwnStatusChange(OwnStatusChange statusChange) {
user.setStatus(statusChange.get());
}
@Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500)
private void onContactsChangedSinceLastLogin() {
contactsChanged = true;
}
@Event(priority = 500)
private void onContactDisabled(ContactDisabled event) {
getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true));
}
/**
* @return a {@code Map<String, User>} of all users stored locally with their user names as keys
* @since Envoy Client v0.2-alpha
*/
public Map<String, Contact> getUsers() { return users; }
public Map<String, User> getUsers() { return users; }
/**
* @return all saved {@link Chat} objects that list the client user as the
* sender
* 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 sender
* @since Envoy Client v0.1-alpha
**/
public List<Chat> getChats() { return chats; }
/**
* @param chats the chats to set
*/
public void setChats(List<Chat> chats) { this.chats = chats; }
public ObservableList<Chat> getChats() { return chats; }
/**
* @return the {@link User} who initialized the local database
@ -134,13 +461,16 @@ public abstract class LocalDB {
* @param idGenerator the message ID generator to set
* @since Envoy Client v0.3-alpha
*/
@Event(priority = 150)
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
/**
* @return {@code true} if an {@link IDGenerator} is present
* @since Envoy Client v0.3-alpha
*/
public boolean hasIDGenerator() { return idGenerator != null; }
public boolean hasIDGenerator() {
return idGenerator != null;
}
/**
* @return the cache map for messages and message status changes
@ -149,57 +479,14 @@ public abstract class LocalDB {
public CacheMap getCacheMap() { return cacheMap; }
/**
* 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
* @return the time stamp when the database was last saved
* @since Envoy Client v0.2-beta
*/
public Optional<Message> getMessage(long id) {
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
}
public Instant getLastSync() { return lastSync; }
/**
* 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
* @return the authentication token of the user
* @since Envoy Client v0.2-beta
*/
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
/**
* 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;
}
});
}
public String getAuthToken() { return authToken; }
}

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;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.prefs.Preferences;
import envoy.util.SerializationUtils;
import dev.kske.eventbus.*;
import dev.kske.eventbus.EventListener;
import envoy.util.*;
import envoy.client.event.EnvoyCloseEvent;
/**
* Manages all application settings, which are different objects that can be
* changed during runtime and serialized them by using either the file system or
* the {@link Preferences} API.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Settings.java</strong><br>
* Created: <strong>11 Nov 2019</strong><br>
* Manages all application settings, which are different objects that can be changed during runtime
* and serialized them by using either the file system or the {@link Preferences} API.
*
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class Settings {
public final class Settings implements EventListener {
// Actual settings accessible by the rest of the application
private Map<String, SettingsItem<?>> items;
@ -30,7 +29,8 @@ public class Settings {
/**
* Settings are stored in this file.
*/
private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
private static final File settingsFile =
new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
/**
* Singleton instance of this class.
@ -38,12 +38,14 @@ public class Settings {
private static Settings settings = new Settings();
/**
* The way to instantiate the settings. Is set to private to deny other
* instances of that object.
* The way to instantiate the settings. Is set to private to deny other instances of that
* object.
*
* @since Envoy Client v0.2-alpha
*/
private Settings() {
EventBus.getInstance().registerListener(this);
// Load settings from settings file
try {
items = SerializationUtils.read(settingsFile, HashMap.class);
@ -67,16 +69,35 @@ public class Settings {
* @throws IOException if an error occurs while saving the themes
* @since Envoy Client v0.2-alpha
*/
public void save() throws IOException {
@Event(eventType = EnvoyCloseEvent.class)
private void save() {
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");
// 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() {
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("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send",
"Sends a message by pressing the enter key."));
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("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"));
}
/**
@ -91,7 +112,9 @@ public class Settings {
* @param themeName the name to set
* @since Envoy Client v0.2-alpha
*/
public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
public void setCurrentTheme(String themeName) {
((SettingsItem<String>) items.get("currentTheme")).set(themeName);
}
/**
* @return true if the currently used theme is one of the default themes
@ -103,9 +126,8 @@ public class Settings {
}
/**
* @return {@code true}, if pressing the {@code Enter} key suffices to send a
* message. Otherwise it has to be pressed in conjunction with the
* {@code Control} key.
* @return {@code true}, if pressing the {@code Enter} key suffices to send a message. Otherwise
* it has to be pressed in conjunction with the {@code Control} key.
* @since Envoy Client v0.2-alpha
*/
public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
@ -113,26 +135,84 @@ public class Settings {
/**
* Changes the keystrokes performed by the user to send a message.
*
* @param enterToSend If set to {@code true} a message can be sent by pressing
* the {@code Enter} key. Otherwise it has to be pressed in
* conjunction with the {@code Control} key.
* @param enterToSend If set to {@code true} a message can be sent by pressing the {@code Enter}
* key. Otherwise it has to be pressed in conjunction with the
* {@code Control} key.
* @since Envoy Client v0.2-alpha
*/
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.
* @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.
*
* @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
*/
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

View File

@ -1,35 +1,26 @@
package envoy.client.data;
import java.io.Serializable;
import java.util.function.Consumer;
import javax.swing.JComponent;
/**
* Encapsulates a persistent value that is directly or indirectly mutable by the
* user.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsItem.java</strong><br>
* Created: <strong>23.12.2019</strong><br>
* Encapsulates a persistent value that is directly or indirectly mutable by the user.
*
* @param <T> the type of this {@link SettingsItem}'s value
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class SettingsItem<T> implements Serializable {
public final class SettingsItem<T> implements Serializable {
private T value;
private String userFriendlyName, description;
private transient Consumer<T> changeHandler;
private static final long serialVersionUID = 1L;
/**
* Initializes a {@link SettingsItem}. The default value's class will be mapped
* to a {@link JComponent} that can be used to display this {@link SettingsItem}
* to the user.
* Initializes a {@link SettingsItem}. The default value's class will be mapped to a
* {@link JComponent} that can be used to display this {@link SettingsItem} to the user.
*
* @param value the default value
* @param userFriendlyName the user friendly name (short)
@ -46,17 +37,18 @@ public class SettingsItem<T> implements Serializable {
* @return the value
* @since Envoy Client v0.3-alpha
*/
public T get() { return value; }
public T get() {
return value;
}
/**
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
* defined, it will be invoked with this value.
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if defined, it
* will be invoked with this value.
*
* @param value the value to set
* @since Envoy Client v0.3-alpha
*/
public void set(T value) {
if (changeHandler != null && value != this.value) changeHandler.accept(value);
this.value = value;
}
@ -70,7 +62,9 @@ public class SettingsItem<T> implements Serializable {
* @param userFriendlyName the userFriendlyName to set
* @since Envoy Client v0.3-alpha
*/
public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
public void setUserFriendlyName(String userFriendlyName) {
this.userFriendlyName = userFriendlyName;
}
/**
* @return the description
@ -83,17 +77,4 @@ public class SettingsItem<T> implements Serializable {
* @since Envoy Client v0.3-alpha
*/
public void setDescription(String description) { this.description = description; }
/**
* Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be
* invoked with the current value once during the registration and every time
* when the value changes.
*
* @param changeHandler the changeHandler to set
* @since Envoy Client v0.3-alpha
*/
public void setChangeHandler(Consumer<T> changeHandler) {
this.changeHandler = changeHandler;
changeHandler.accept(value);
}
}

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.
* <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
* @since Envoy Client v0.1-beta
@ -26,7 +22,9 @@ public final class AudioPlayer {
*
* @since Envoy Client v0.1-beta
*/
public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
public AudioPlayer() {
this(AudioRecorder.DEFAULT_AUDIO_FORMAT);
}
/**
* Initializes the player with a given audio format.

View File

@ -1,8 +1,7 @@
package envoy.client.data.audio;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.*;
import javax.sound.sampled.*;
@ -10,10 +9,6 @@ import envoy.exception.EnvoyException;
/**
* 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
* @since Envoy Client v0.1-beta
@ -25,7 +20,13 @@ public final class AudioRecorder {
*
* @since Envoy Client v0.1-beta
*/
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 DataLine.Info info;
@ -38,7 +39,9 @@ public final class AudioRecorder {
*
* @since Envoy Client v0.1-beta
*/
public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
public AudioRecorder() {
this(DEFAULT_AUDIO_FORMAT);
}
/**
* Initializes the recorder with a given audio format.
@ -78,7 +81,7 @@ public final class AudioRecorder {
line.start();
// Prepare temp file
tempFile = Files.createTempFile("recording", "wav");
tempFile = Files.createTempFile("recording", FILE_FORMAT);
// Start the recording
final var ais = new AudioInputStream(line);
@ -117,6 +120,6 @@ public final class AudioRecorder {
line.close();
try {
Files.deleteIfExists(tempFile);
} catch (IOException e) {}
} catch (final IOException e) {}
}
}

View File

@ -1,10 +1,6 @@
/**
* 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
* @since Envoy Client v0.1-beta
*/

View File

@ -0,0 +1,20 @@
package envoy.client.data.commands;
import java.util.List;
/**
* 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 Callable {
/**
* Performs the instance specific action when a {@link SystemCommand} has been called.
*
* @param arguments the arguments that should be passed to the {@link SystemCommand}
* @since Envoy Client v0.2-beta
*/
void call(List<String> arguments);
}

View File

@ -0,0 +1,116 @@
package envoy.client.data.commands;
import java.util.*;
import java.util.function.Consumer;
/**
* 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 Callable {
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 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; }
@Override
public void call(List<String> arguments) {
action.accept(arguments);
++relevance;
}
/**
* @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 var other = (SystemCommand) obj;
return Objects.equals(action, other.action);
}
@Override
public String toString() {
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments
+ ", "
+ (description != null ? "description=" + description + ", " : "")
+ (defaults != null ? "defaults=" + defaults : "") + "]";
}
}

View File

@ -0,0 +1,232 @@
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,336 @@
package envoy.client.data.commands;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.logging.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.util.EnvoyLog;
/**
* Stores all {@link SystemCommand}s used. SystemCommands can be called using an activator char and
* the text that needs to be present behind the activator. Additionally offers the option to request
* recommendations for a partial input String.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class SystemCommandMap {
private final Character activator;
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);
/**
* Creates a new {@code SystemCommandMap} with the given char as activator. If this Character is
* null, any text used as input will be treated as a system command.
*
* @param activator the char to use as activator for commands
* @since Envoy Client v0.3-beta
*/
public SystemCommandMap(Character activator) {
this.activator = activator;
}
/**
* Creates a new {@code SystemCommandMap} with '/' as activator.
*
* @since Envoy Client v0.3-beta
*/
public SystemCommandMap() {
activator = '/';
}
/**
* 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.
* <p>
* Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
* {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, ""));}<br>
* {@code ....}<br>
* user input: {@code "*example xyz ..."}<br>
* {@code systemCommands.get("example xyz ...")} or
* {@code systemCommands.get("*example xyz ...")} result:
* {@code Optional<SystemCommand>.get() != null}
*
* @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 activator of a {@link SystemCommand} is stripped.<br>
* It only checks the word beginning from the first non-blank position in the input. It returns
* the command as (most likely) entered as key in the map for the first word of the text.<br>
* Activators in the middle of the word will be disregarded.
*
* @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 activator is not a system command. Only exception: for recommendation
* purposes.
*/
public String getCommand(String raw) {
final var trimmed = raw.stripLeading();
// Entering only the activator should not throw an error
if (trimmed.length() == 1 && activator != null && activator.equals(trimmed.charAt(0)))
return "";
else {
final var index = trimmed.indexOf(' ');
return trimmed.substring(
activator != null && activator.equals(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 var valid = commandPattern.matcher(command).matches();
if (!valid)
logger.log(Level.WARNING,
"The command \"" + command
+ "\" is not valid. As it might cause problems when executed, 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 the activator is the first visible
* character and then checks if a command is present after that activator. If that is the case,
* it will be executed.
*
* @param raw the raw input string
* @return whether a command could be found and successfully executed
* @since Envoy Client v0.2-beta
*/
public boolean executeIfPresent(String raw) {
// possibly a command was detected and could be executed
final var raw2 = raw.stripLeading();
final var commandFound = activator == null || raw2.startsWith(activator.toString())
? executeAvailableCommand(raw2)
: false;
// the command was executed successfully - no further checking needed
if (commandFound)
logger.log(Level.FINE, "executed system command " + getCommand(raw2));
return commandFound;
}
/**
* 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 activator is at
* its beginning or not.<br>
* If recommendations are present, the given function will be executed on the
* recommendations.<br>
* Otherwise nothing will be done.<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);
}
/**
* This method checks if the input String is a key in the map and executes the wrapped System
* command if present.
* <p>
* Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
* {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", new SystemCommand(text -> {button.setText(text.get(0))},
* 1, null, ""));}<br>
* {@code ....}<br>
* user input: {@code "*example xyz ..."}<br>
* {@code systemCommands.executeIfPresent("example xyz ...")} or
* {@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 and successfully executed
* @since Envoy Client v0.2-beta
*/
private boolean executeAvailableCommand(String input) {
final var command = getCommand(input);
final var value = get(command);
final var commandExecuted = new AtomicBoolean(value.isPresent());
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.call(arguments);
} catch (final NumberFormatException e) {
logger.log(Level.INFO,
String.format(
"System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.",
command));
Platform.runLater(() -> {
final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Please enter a readable number as argument.");
alert.showAndWait();
});
commandExecuted.set(false);
} catch (final Exception e) {
logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e);
Platform.runLater(() -> {
final var alert = new Alert(AlertType.ERROR);
alert.setContentText(
"Could not execute system command: Internal error. Please insult the responsible programmer.");
alert.showAndWait();
});
commandExecuted.set(false);
}
});
return commandExecuted.get();
}
/**
* 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;
}
/**
* Recommends commands based upon the currently entered input.<br>
* In the current implementation, all that gets checked 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>
*
* @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 (var 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; }
/**
* @return the activator of any command in this map. Can be null.
* @since Envoy Client v0.3-beta
*/
public Character getActivator() { return activator; }
}

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,77 @@
package envoy.client.data.shortcuts;
import javafx.scene.input.*;
import envoy.data.User.UserStatus;
import envoy.client.data.Context;
import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.util.UserUtil;
/**
* Envoy-specific implementation of the keyboard-shortcut interaction offered by
* {@link GlobalKeyShortcuts}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class EnvoyShortcutConfig {
private EnvoyShortcutConfig() {}
/**
* Supplies the default shortcuts for {@link GlobalKeyShortcuts}.
*
* @since Envoy Client v0.3-beta
*/
public static void initializeEnvoyShortcuts() {
final var instance = GlobalKeyShortcuts.getInstance();
// Add the option to exit with "Control" + "Q" or "Alt" + "F4" as offered by
// some desktop environments
instance.add(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN),
ShutdownHelper::exit);
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
instance.addForNotExcluded(
new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN,
KeyCombination.SHIFT_DOWN),
UserUtil::logout,
SceneInfo.LOGIN_SCENE);
// Add option to open settings scene with "Control"+"S", if not in login scene
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN),
() -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE),
SceneInfo.SETTINGS_SCENE,
SceneInfo.LOGIN_SCENE);
// Add option to change to status away
instance.addForNotExcluded(
new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN,
KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.AWAY),
SceneInfo.LOGIN_SCENE);
// Add option to change to status busy
instance.addForNotExcluded(
new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN,
KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.BUSY),
SceneInfo.LOGIN_SCENE);
// Add option to change to status offline
instance.addForNotExcluded(
new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN,
KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.OFFLINE),
SceneInfo.LOGIN_SCENE);
// Add option to change to status online
instance.addForNotExcluded(
new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN,
KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.ONLINE),
SceneInfo.LOGIN_SCENE);
}
}

View File

@ -0,0 +1,79 @@
package envoy.client.data.shortcuts;
import java.util.*;
import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext.SceneInfo;
/**
* Contains all keyboard shortcuts used throughout the application.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class GlobalKeyShortcuts {
private final EnumMap<SceneInfo, Map<KeyCombination, Runnable>> shortcuts =
new EnumMap<>(SceneInfo.class);
private static GlobalKeyShortcuts instance = new GlobalKeyShortcuts();
private GlobalKeyShortcuts() {
for (final var sceneInfo : SceneInfo.values())
shortcuts.put(sceneInfo, new HashMap<KeyCombination, Runnable>());
}
/**
* @return the instance of global keyboard shortcuts.
* @since Envoy Client v0.3-beta
*/
public static GlobalKeyShortcuts getInstance() { return instance; }
/**
* Adds the given keyboard shortcut and its action to all scenes.
*
* @param keys the keys to press to perform the given action
* @param action the action to perform
* @since Envoy Client v0.3-beta
*/
public void add(KeyCombination keys, Runnable action) {
shortcuts.values().forEach(collection -> collection.put(keys, action));
}
/**
* Adds the given keyboard shortcut and its action to all scenes that are not part of exclude.
*
* @param keys the keys to press to perform the given action
* @param action the action to perform
* @param exclude the scenes that should be excluded from receiving this keyboard shortcut
* @since Envoy Client v0.3-beta
*/
public void addForNotExcluded(KeyCombination keys, Runnable action, SceneInfo... exclude) {
// Computing the remaining sceneInfos
final var include = new SceneInfo[SceneInfo.values().length - exclude.length];
int index = 0;
outer: for (final var sceneInfo : SceneInfo.values()) {
for (final var excluded : exclude)
if (sceneInfo.equals(excluded))
continue outer;
include[index++] = sceneInfo;
}
// Adding the action to the remaining sceneInfos
for (final var sceneInfo : include)
shortcuts.get(sceneInfo).put(keys, action);
}
/**
* Returns all stored keyboard shortcuts for the given scene constant.
*
* @param sceneInfo the currently loading scene
* @return all stored keyboard shortcuts for this scene
* @since Envoy Client v0.3-beta
*/
public Map<KeyCombination, Runnable> getKeyboardShortcuts(SceneInfo sceneInfo) {
return shortcuts.get(sceneInfo);
}
}

View File

@ -0,0 +1,24 @@
package envoy.client.data.shortcuts;
import java.util.Map;
import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext;
/**
* Provides methods to set the keyboard shortcuts for a specific scene. Should only be implemented
* by controllers of scenes so that these methods can automatically be called inside
* {@link SceneContext} as soon as the underlying FXML file has been loaded.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public interface KeyboardMapping {
/**
* @return all keyboard shortcuts of a scene
* @since Envoy Client v0.3-beta
*/
Map<KeyCombination, Runnable> getKeyboardShortcuts();
}

View File

@ -0,0 +1,7 @@
/**
* Contains the necessary classes to enable using keyboard shortcuts in Envoy.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
package envoy.client.data.shortcuts;

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,23 @@
package envoy.client.event;
import envoy.data.Contact;
import envoy.event.Event;
/**
* Signifies that the chat of a contact should be disabled.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class ContactDisabled extends Event<Contact> {
private static final long serialVersionUID = 1L;
/**
* @param contact the contact that should be disabled
* @since Envoy Client v0.3-beta
*/
public ContactDisabled(Contact contact) {
super(contact);
}
}

View File

@ -0,0 +1,15 @@
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

@ -0,0 +1,22 @@
package envoy.client.event;
import envoy.event.Event;
/**
* Conveys the deletion of a message.
*
* @author Leon Hofmeister
* @since Envoy Common v0.3-beta
*/
public class MessageDeletion extends Event<Long> {
private static final long serialVersionUID = 1L;
/**
* @param messageID the ID of the deleted message
* @since Envoy Common v0.3-beta
*/
public MessageDeletion(long messageID) {
super(messageID);
}
}

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

@ -0,0 +1,23 @@
package envoy.client.event;
import envoy.data.User.UserStatus;
import envoy.event.Event;
/**
* Signifies a manual status change of the client user.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class OwnStatusChange extends Event<UserStatus> {
private static final long serialVersionUID = 1L;
/**
* @param value the new user status of the client user
* @since Envoy Client v0.3-beta
*/
public OwnStatusChange(UserStatus value) {
super(value);
}
}

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;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ThemeChangeEvent.java</strong><br>
* Created: <strong>15 Dec 2019</strong><br>
* Notifies UI components of a theme change.
*
* @author Kai S. K. Engelbart
* @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;
/**
* 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,33 @@
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.setHeaderText("");
if (Settings.getInstance().isAskForConfirmation())
alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
else
action.run();
}
}

View File

@ -0,0 +1,45 @@
package envoy.client.helper;
import dev.kske.eventbus.EventBus;
import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent;
import envoy.client.ui.StatusTrayIcon;
/**
* 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()} and {@link StatusTrayIcon#isSupported()}.
*
* @since Envoy Client v0.2-beta
*/
public static void exit() {
exit(false);
}
/**
* Exits Envoy immediately if {@code force = true}, else it can exit or minimize Envoy,
* depending on the current state of {@link Settings#isHideOnClose()} and
* {@link StatusTrayIcon#isSupported()}.
*
* @param force whether to close in any case.
* @since Envoy Client v0.2-beta
*/
public static void exit(boolean force) {
if (!force && Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
Context.getInstance().getStage().setIconified(true);
else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0);
}
}
}

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,30 @@
package envoy.client.net;
import java.io.Closeable;
import java.io.IOException;
import java.io.*;
import java.net.Socket;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.*;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
import envoy.client.data.*;
import envoy.client.event.SendEvent;
import envoy.data.*;
import envoy.event.*;
import envoy.event.contact.ContactOperation;
import envoy.event.contact.ContactSearchResult;
import envoy.util.EnvoyLog;
import envoy.util.SerializationUtils;
import envoy.util.*;
import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent;
/**
* Establishes a connection to the server, performs a handshake and delivers
* 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>
* Establishes a connection to the server, performs a handshake and delivers certain objects to the
* server.
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @author Leon Hofmeister
* @since Envoy Client v0.1-alpha
*/
public class Client implements Closeable {
public final class Client implements EventListener, Closeable {
// Connection handling
private Socket socket;
@ -46,35 +41,45 @@ public class Client implements Closeable {
private static final EventBus eventBus = EventBus.getInstance();
/**
* 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
* will block for up to 5 seconds. If the handshake does exceed this time limit,
* an exception is thrown.
* 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 connection has to be
* established and a handshake has to be made, this method will block for up to 5 seconds. If
* the handshake does exceed this time limit, an exception is thrown.
*
* @param credentials the login credentials of the user
* @param cacheMap the map of all caches needed
* @throws TimeoutException if the server could not be reached
* @throws IOException if the login credentials could not be written
* @throws InterruptedException if the current thread is interrupted while
* waiting for the handshake response
* @throws InterruptedException if the current thread is interrupted while waiting for the
* handshake response
*/
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
if (online) throw new IllegalStateException("Handshake has already been performed successfully");
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap)
throws TimeoutException, IOException, InterruptedException {
if (online)
throw new IllegalStateException("Handshake has already been performed successfully");
rejected = false;
// Establish TCP connection
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...",
config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort());
logger.log(Level.FINE, "Successfully established TCP connection to server");
// Create object receiver
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.registerProcessors(cacheMap.getMap());
receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
rejected = false;
// Start receiver
receiver.start();
@ -94,24 +99,24 @@ public class Client implements Closeable {
return;
}
if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
if (System.currentTimeMillis() - start > 5000) {
rejected = true;
throw new TimeoutException("Did not log in after 5 seconds");
}
Thread.sleep(500);
}
online = true;
logger.log(Level.INFO, "Handshake completed.");
}
/**
* Initializes the {@link Receiver} used to process data sent from the server to
* this client.
* Initializes the {@link Receiver} used to process data sent from the server to this client.
*
* @param localDB the local database used to persist the current
* {@link IDGenerator}
* @param localDB the local database used to persist the current {@link IDGenerator}
* @param cacheMap the map of all caches needed
* @throws IOException if no {@link IDGenerator} is present and none could be
* requested from the server
* @throws IOException if no {@link IDGenerator} is present and none could be requested from the
* server
* @since Envoy Client v0.2-alpha
*/
public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
@ -120,99 +125,92 @@ public class Client implements Closeable {
// Remove all processors as they are only used during the handshake
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
cacheMap.get(Message.class).setProcessor(receivedMessageProcessor);
cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor);
cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor);
cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor);
// 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);
}
});
cacheMap.get(Message.class).setProcessor(eventBus::dispatch);
cacheMap.get(GroupMessage.class).setProcessor(eventBus::dispatch);
cacheMap.get(MessageStatusChange.class).setProcessor(eventBus::dispatch);
cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch);
// 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
cacheMap.getMap().values().forEach(Cache::relay);
}
/**
* Sends a message to the server. The message's status will be incremented once
* it was delivered successfully.
* 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 (final IOException e) {
throw new RuntimeException(e);
}
}
/**
* Sends a message to the server. The message's status will be incremented once it was delivered
* successfully.
*
* @param message the message to send
* @throws IOException if the message does not reach the server
* @since Envoy Client v0.3-alpha
*/
public void sendMessage(Message message) throws IOException {
writeObject(message);
public void sendMessage(Message message) {
send(message);
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.
*
* @throws IOException if the request does not reach the server
* @since Envoy Client v0.3-alpha
*/
public void requestIdGenerator() throws IOException {
public void requestIDGenerator() {
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
public void close() throws IOException { if (online) socket.close(); }
@Event(eventType = EnvoyCloseEvent.class, priority = 50)
public void close() {
if (online) {
logger.log(Level.INFO, "Closing connection...");
try {
private void writeObject(Object obj) throws IOException {
checkOnline();
logger.log(Level.FINE, "Sending " + obj);
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
// The sender must be reset as otherwise the handshake is immediately closed
sender = null;
online = false;
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
@ -230,6 +228,7 @@ public class Client implements Closeable {
/**
* @return the {@link Receiver} used by this {@link Client}
* @since v0.2-alpha
*/
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,30 @@
package envoy.client.net;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.*;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.*;
import envoy.util.EnvoyLog;
import envoy.util.SerializationUtils;
import dev.kske.eventbus.*;
import envoy.util.*;
/**
* Receives objects from the server and passes them to processor objects based
* on their class.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Receiver.java</strong><br>
* Created: <strong>30.12.2019</strong><br>
* Receives objects from the server and passes them to processor objects based on their class.
*
* @author Kai S. K. Engelbart
* @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 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}.
@ -40,18 +35,18 @@ public class Receiver extends Thread {
public Receiver(InputStream in) {
super("Receiver");
this.in = in;
setDaemon(true);
}
/**
* Starts the receiver loop. When an object is read, it is passed to the
* appropriate processor.
* Starts the receiver loop. When an object is read, it is passed to the appropriate processor.
*
* @since Envoy Client v0.3-alpha
*/
@Override
public void run() {
while (true) {
while (isAlive)
try {
// Read object length
final byte[] lenBytes = new byte[4];
@ -66,53 +61,79 @@ public class Receiver extends Thread {
// Catch LV encoding errors
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,
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;
}
try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
try (ObjectInputStream oin =
new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
final Object obj = oin.readObject();
logger.log(Level.FINE, "Received " + obj);
// Get appropriate processor
@SuppressWarnings("rawtypes")
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()));
else processor.accept(obj);
// Dispatch to the processor if present
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.
logger.log(Level.INFO, "Exiting receiver...");
return;
} catch (final Exception e) {
logger.log(Level.SEVERE, "Error on receiver thread", e);
}
}
}
/**
* Adds an object processor to this {@link Receiver}. It will be called once an
* object of the accepted class has been received.
* Adds an object processor to this {@link Receiver}. It will be called once an object of the
* accepted class has been received.
*
* @param processorClass the object class accepted by the processor
* @param processor the object processor
* @since Envoy Client v0.3-alpha
*/
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { processors.put(processorClass, processor); }
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) {
processors.put(processorClass, processor);
}
/**
* Adds a map of object processors to this {@link Receiver}.
*
*
* @param processors the processors to add the processors to add
* @since Envoy Client v0.1-beta
*/
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { this.processors.putAll(processors); }
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) {
this.processors.putAll(processors);
}
/**
* Removes all object processors registered at this {@link Receiver}.
*
* @since Envoy Client v0.3-alpha
*/
public void removeAllProcessors() { processors.clear(); }
public void removeAllProcessors() {
processors.clear();
}
}

View File

@ -1,28 +1,21 @@
package envoy.client.net;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.*;
import envoy.client.data.Cache;
import envoy.client.data.LocalDB;
import envoy.data.Message;
import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog;
import envoy.client.data.*;
/**
* Implements methods to send {@link Message}s and
* {@link MessageStatusChange}s to the server or cache them inside a
* {@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>
* Implements methods to send {@link Message}s and {@link MessageStatusChange}s to the server or
* cache them inside a {@link LocalDB} depending on the online status.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class WriteProxy {
public final class WriteProxy {
private final Client client;
private final LocalDB localDB;
@ -30,13 +23,11 @@ public class WriteProxy {
private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
/**
* Initializes a write proxy using a client and a local database. The
* corresponding cache processors are injected into the caches.
* Initializes a write proxy using a client and a local database. The corresponding cache
* processors are injected into the caches.
*
* @param client the client used to send messages and message status change
* events
* @param localDB the local database used to cache messages and message status
* change events
* @param client the client instance used to send messages and events if online
* @param localDB the local database used to cache messages and events if offline
* @since Envoy Client v0.3-alpha
*/
public WriteProxy(Client client, LocalDB localDB) {
@ -45,26 +36,17 @@ public class WriteProxy {
// Initialize cache processors for messages and message status change events
localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
try {
logger.log(Level.FINER, "Sending cached " + msg);
client.sendMessage(msg);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Could not send cached message: ", e);
}
logger.log(Level.FINER, "Sending cached " + msg);
client.sendMessage(msg);
});
localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
logger.log(Level.FINER, "Sending cached " + evt);
try {
client.sendEvent(evt);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
}
client.send(evt);
});
}
/**
* Sends cached {@link Message}s and {@link MessageStatusChange}s to the
* server.
* Sends cached {@link Message}s and {@link MessageStatusChange}s to the server.
*
* @since Envoy Client v0.3-alpha
*/
@ -73,28 +55,30 @@ public class WriteProxy {
}
/**
* Delivers a message to the server if online. Otherwise the message is cached
* inside the local database.
* Delivers a message to the server if online. Otherwise the message is cached inside the local
* database.
*
* @param message the message to send
* @throws IOException if the message could not be sent
* @since Envoy Client v0.3-alpha
*/
public void writeMessage(Message message) throws IOException {
if (client.isOnline()) client.sendMessage(message);
else localDB.getCacheMap().getApplicable(Message.class).accept(message);
public void writeMessage(Message message) {
if (client.isOnline())
client.sendMessage(message);
else
localDB.getCacheMap().getApplicable(Message.class).accept(message);
}
/**
* Delivers a message status change event to the server if online. Otherwise the
* event is cached inside the local database.
* Delivers a message status change event to the server if online. Otherwise the event is cached
* inside the local database.
*
* @param evt the event to send
* @throws IOException if the event could not be sent
* @since Envoy Client v0.3-alpha
*/
public void writeMessageStatusChange(MessageStatusChange evt) throws IOException {
if (client.isOnline()) client.sendEvent(evt);
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
public void writeMessageStatusChange(MessageStatusChange evt) {
if (client.isOnline())
client.send(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

@ -1,12 +1,8 @@
package envoy.client.ui;
/**
* This interface defines an action that should be performed when a scene gets
* 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>
* This interface defines an action that should be performed when a scene gets restored from the
* scene stack in {@link SceneContext}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
@ -16,8 +12,7 @@ public interface Restorable {
/**
* This method is getting called when a scene gets restored.<br>
* Hence, it can contain anything that should be done when the underlying scene
* gets restored.
* Hence, it can contain anything that should be done when the underlying scene gets restored.
*
* @since Envoy Client v0.1-beta
*/

View File

@ -4,32 +4,29 @@ import java.io.IOException;
import java.util.Stack;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.*;
import javafx.stage.Stage;
import envoy.client.data.Settings;
import envoy.client.event.ThemeChangeEvent;
import envoy.event.EventBus;
import dev.kske.eventbus.*;
import envoy.util.EnvoyLog;
import envoy.client.data.Settings;
import envoy.client.data.shortcuts.*;
import envoy.client.event.*;
/**
* 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
* displayed.
* 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 displayed.
* <p>
* When a scene is loaded, the style sheet for the current theme is applied to
* it.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SceneContext.java</strong><br>
* Created: <strong>06.06.2020</strong><br>
* When a scene is loaded, the style sheet for the current theme is applied to it.
*
* @author Kai S. K. Engelbart
* @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.
@ -53,40 +50,21 @@ public final class SceneContext {
*/
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.
*
* @since Envoy Client v0.1-beta
*/
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");
LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) { this.path = path; }
SceneInfo(String path) {
this.path = path;
}
}
private final Stage stage;
@ -104,7 +82,7 @@ public final class SceneContext {
*/
public SceneContext(Stage stage) {
this.stage = stage;
EventBus.getInstance().register(ThemeChangeEvent.class, theme -> applyCSS());
EventBus.getInstance().registerListener(this);
}
/**
@ -115,21 +93,39 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta
*/
public void load(SceneInfo sceneInfo) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
loader.setRoot(null);
loader.setController(null);
try {
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var rootNode =
(Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode);
controllerStack.push(loader.getController());
final var controller = loader.getController();
controllerStack.push(controller);
sceneStack.push(scene);
stage.setScene(scene);
applyCSS();
// Supply the global custom keyboard shortcuts for that scene
scene.getAccelerators()
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo));
// Supply the scene specific keyboard shortcuts
if (controller instanceof KeyboardMapping)
scene.getAccelerators()
.putAll(((KeyboardMapping) controller).getKeyboardShortcuts());
// 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();
Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE));
applyCSS();
stage.show();
} 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);
throw new RuntimeException(e);
}
}
@ -140,8 +136,12 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta
*/
public void pop() {
// Pop scene and controller
sceneStack.pop();
controllerStack.pop();
// Apply new scene if present
if (!sceneStack.isEmpty()) {
final var newScene = sceneStack.peek();
stage.setScene(newScene);
@ -150,7 +150,8 @@ public final class SceneContext {
// If the controller implements the Restorable interface,
// the actions to perform on restoration will be executed here
final var controller = controllerStack.peek();
if (controller instanceof Restorable) ((Restorable) controller).onRestore();
if (controller instanceof Restorable)
((Restorable) controller).onRestore();
}
stage.show();
}
@ -160,10 +161,22 @@ public final class SceneContext {
final var styleSheets = stage.getScene().getStylesheets();
final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css";
styleSheets.clear();
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm());
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(),
getClass().getResource(themeCSS).toExternalForm());
}
}
@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
* @return the controller used by the current scene
@ -176,4 +189,10 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta
*/
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,33 +1,31 @@
package envoy.client.ui;
import java.io.File;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.*;
import java.time.Instant;
import java.util.concurrent.TimeoutException;
import java.util.logging.*;
import javafx.application.Application;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene;
import envoy.data.GroupMessage;
import envoy.data.Message;
import envoy.event.GroupMessageStatusChange;
import envoy.event.MessageStatusChange;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.*;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.client.data.*;
import envoy.client.data.shortcuts.EnvoyShortcutConfig;
import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene;
import envoy.client.util.IconUtil;
/**
* Handles application startup and shutdown.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Startup.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
* Handles application startup.
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
@ -40,96 +38,210 @@ public final class Startup extends Application {
*
* @since Envoy Client v0.1-beta
*/
public static final String VERSION = "0.1-beta";
public static final String VERSION = "0.2-beta";
private LocalDB localDB;
private Client client;
private static LocalDB localDB;
private static final Context context = Context.getInstance();
private static final Client client = context.getClient();
private static final ClientConfig config = ClientConfig.getInstance();
private static final Logger logger = EnvoyLog.getLogger(Startup.class);
/**
* Loads the configuration, initializes the client and the local database and
* delegates the rest of the startup process to {@link LoginScene}.
* Loads the configuration, initializes the client and the local database and delegates the rest
* of the startup process to {@link LoginScene}.
*
* @since Envoy Client v0.1-beta
*/
@Override
public void start(Stage stage) throws Exception {
// Initialize config and logger
try {
// Load the configuration from client.properties first
final Properties properties = new Properties();
properties.load(Startup.class.getClassLoader().getResourceAsStream("client.properties"));
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) {
config.loadAll(Startup.class, "client.properties",
getParameters().getRaw().toArray(new String[0]));
EnvoyLog.initialize(config);
} catch (final IllegalStateException e) {
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
logger.log(Level.SEVERE, "Error loading configuration values: ", e);
e.printStackTrace();
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...");
// Initialize the local database
if (config.isIgnoreLocalDB()) {
localDB = new TransientLocalDB();
new Alert(AlertType.WARNING, "Ignoring local database.\nMessages will not be saved!").showAndWait();
} else try {
localDB = new PersistentLocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
} catch (final IOException e3) {
logger.log(Level.SEVERE, "Could not initialize local database: ", e3);
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e3).showAndWait();
try {
final var localDBFile = new File(config.getHomeDirectory(), config.getServer());
logger.info("Initializing LocalDB at " + localDBFile);
localDB = new LocalDB(localDBFile);
} catch (IOException | EnvoyException e) {
logger.log(Level.SEVERE, "Could not initialize local database: ", e);
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
System.exit(1);
return;
}
// Initialize client and unread message cache
client = new Client();
// Prepare handshake
context.setLocalDB(localDB);
// Configure stage
stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
// Configure global shortcuts
EnvoyShortcutConfig.initializeEnvoyShortcuts();
// 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();
cacheMap.put(Message.class, new Cache<Message>());
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
final var originalStatus =
localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus();
try {
client.performHandshake(credentials, cacheMap);
if (client.isOnline()) {
stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
final var sceneContext = new SceneContext(stage);
sceneContext.load(SceneInfo.LOGIN_SCENE);
sceneContext.<LoginScene>getController().initializeData(client, localDB, cacheMap, sceneContext);
// Restore the original status as the server automatically returns status ONLINE
client.getSender().setStatus(originalStatus);
loadChatScene();
client.initReceiver(localDB, cacheMap);
return true;
} 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 void stop() {
public static boolean attemptOfflineMode(String identifier) {
try {
logger.log(Level.INFO, "Closing connection...");
client.close();
logger.log(Level.INFO, "Saving local database and settings...");
localDB.save();
Settings.getInstance().save();
logger.log(Level.INFO, "Envoy was terminated by its user");
// Try entering offline mode
final User clientUser = localDB.getUsers().get(identifier);
if (clientUser == null)
throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser);
loadChatScene();
return true;
} 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
final var user = client.getSender();
localDB.setUser(user);
// 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();
// Inform the server that this user has a different user status than expected
if (!user.getStatus().equals(UserStatus.ONLINE))
client.send(new UserStatusChange(user));
} 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();
// Exit or minimize the stage when a close request occurs
stage.setOnCloseRequest(
e -> {
ShutdownHelper.exit();
if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
e.consume();
});
// Initialize status tray icon
if (StatusTrayIcon.isSupported())
new StatusTrayIcon(stage).show();
// Start auto save thread
localDB.initAutoSave();
}
}

View File

@ -1,100 +1,224 @@
package envoy.client.ui;
import static java.awt.Image.SCALE_SMOOTH;
import java.awt.*;
import java.awt.TrayIcon.MessageType;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.logging.Level;
import java.awt.image.BufferedImage;
import javafx.application.Platform;
import javafx.stage.Stage;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
import envoy.client.event.MessageCreationEvent;
import envoy.data.Message;
import envoy.event.EventBus;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.data.User.UserStatus;
import envoy.client.data.*;
import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper;
import envoy.client.util.*;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>StatusTrayIcon.java</strong><br>
* Created: <strong>3 Dec 2019</strong><br>
* A tray icon with the Envoy logo, an "Envoy" tool tip and a pop-up menu with menu items for
* <ul>
* <li>Changing the user status</li>
* <li>Logging out</li>
* <li>Quitting Envoy</li>
* </ul>
*
* @author Kai S. K. Engelbart
* @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
* system tray. This includes displaying the icon, but also creating
* notifications when new messages are received.
* The {@link TrayIcon} provided by the System Tray API for controlling the system tray. This
* includes displaying the icon, but also creating notifications when new messages are received.
*/
private final TrayIcon trayIcon;
/**
* A received {@link Message} is only displayed as a system tray notification if
* this variable is set to {@code true}.
* A received {@link Message} is only displayed as a system tray notification if this variable
* is set to {@code true}.
*/
private boolean displayMessages = false;
private boolean displayMessageNotification;
/**
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
* menu.
*
* @param focusTarget the {@link Window} which focus determines if message
* notifications are displayed
* @throws EnvoyException if the currently used OS does not support the System
* Tray API
* @since Envoy Client v0.2-alpha
* The size of the tray icon's image.
*/
public StatusTrayIcon(Window focusTarget) throws EnvoyException {
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported.");
private final Dimension size;
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.setToolTip("You are notified if you have unread messages.");
/**
* The Envoy logo on which the current user status and unread message count will be drawn to
* compose the tray icon.
*/
private final Image logo;
final PopupMenu popup = new PopupMenu();
private static final Font unreadMessageFont = new Font("sans-serif", Font.PLAIN, 8);
final MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> System.exit(0));
/**
* @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 menu.
*
* @param stage the stage whose focus determines if message notifications are displayed
* @since Envoy Client v0.2-beta
*/
public StatusTrayIcon(Stage stage) {
size = SystemTray.getSystemTray().getTrayIconSize();
logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png").getScaledInstance(size.width,
size.height, SCALE_SMOOTH);
final var popup = new PopupMenu();
// Adding the exit menu item
final var exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit(true));
popup.add(exitMenuItem);
trayIcon.setPopupMenu(popup);
// Adding the logout menu item
final var logoutMenuItem = new MenuItem("Logout");
logoutMenuItem.addActionListener(evt -> Platform.runLater(UserUtil::logout));
popup.add(logoutMenuItem);
// Only display messages if the chat window is not focused
focusTarget.addWindowFocusListener(new WindowAdapter() {
// Adding the status change items
final var statusSubMenu = new Menu("Change status");
for (final var status : UserStatus.values()) {
final var statusMenuItem = new MenuItem(status.toString().toLowerCase());
statusMenuItem
.addActionListener(evt -> Platform.runLater(() -> UserUtil.changeStatus(status)));
statusSubMenu.add(statusMenuItem);
}
popup.add(statusSubMenu);
@Override
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
// Initialize the icon
trayIcon = new TrayIcon(createImage(), "Envoy", popup);
@Override
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
});
// Only display messages if the stage is not focused and the current user status
// is not BUSY (if BUSY, displayMessageNotification will be false)
stage.focusedProperty()
.addListener((ov, wasFocused, isFocused) -> displayMessageNotification =
!displayMessageNotification && wasFocused ? false : !isFocused);
// Listen to changes in the total unread message amount
Chat.getTotalUnreadAmount().addListener((ov, oldValue, newValue) -> updateImage());
// 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
// TODO: Handle other message types
EventBus.getInstance()
.register(MessageCreationEvent.class,
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
EventBus.getInstance().registerListener(this);
}
/**
* 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
*/
public void show() throws EnvoyException {
public void show() {
try {
SystemTray.getSystemTray().add(trayIcon);
} 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);
} catch (AWTException e) {}
}
/**
* Removes the icon from the system tray.
*
* @since Envoy Client v0.2-beta
*/
@Event(eventType = Logout.class)
public void hide() {
SystemTray.getSystemTray().remove(trayIcon);
}
@Event
private void onOwnStatusChange(OwnStatusChange statusChange) {
displayMessageNotification = !statusChange.get().equals(UserStatus.BUSY);
trayIcon.getImage().flush();
trayIcon.setImage(createImage());
}
@Event
private void onMessage(Message message) {
if (displayMessageNotification)
trayIcon
.displayMessage(message.hasAttachment()
? "New " + message.getAttachment().getType().toString().toLowerCase()
+ " message received"
: "New message received", message.getText(), MessageType.INFO);
}
/**
* Updates the tray icon's image by first releasing the resources held by the current image and
* then setting a new one generated by the {@link StatusTrayIcon#createImage()} method.
*
* @since Envoy Client v0.3-beta
*/
private void updateImage() {
trayIcon.getImage().flush();
trayIcon.setImage(createImage());
}
/**
* Composes an icon that displays the current user status and the amount of unread messages, if
* any are present.
*
* @since Envoy Client v0.3-beta
*/
private BufferedImage createImage() {
// Create a new image with the dimensions of the logo
var img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
// Obtain the draw graphics of the image and copy the logo
var g = img.createGraphics();
g.drawImage(logo, 0, 0, null);
// Draw the current user status
switch (Context.getInstance().getLocalDB().getUser().getStatus()) {
case ONLINE:
g.setColor(Color.GREEN);
break;
case AWAY:
g.setColor(Color.ORANGE);
break;
case BUSY:
g.setColor(Color.RED);
break;
case OFFLINE:
g.setColor(Color.GRAY);
}
g.fillOval(size.width / 2, size.height / 2, size.width / 2, size.height / 2);
// Draw total amount of unread messages, if any are present
if (Chat.getTotalUnreadAmount().get() > 0) {
// Draw black background circle
g.setColor(Color.BLACK);
g.fillOval(size.width / 2, 0, size.width / 2, size.height / 2);
// Unread amount in white
String unreadAmount = Chat.getTotalUnreadAmount().get() > 9 ? "9+"
: String.valueOf(Chat.getTotalUnreadAmount().get());
g.setColor(Color.WHITE);
g.setFont(unreadMessageFont);
g.drawString(unreadAmount,
3 * size.width / 4 - g.getFontMetrics().stringWidth(unreadAmount) / 2,
size.height / 2);
}
// Finish drawing
g.dispose();
return img;
}
}

View File

@ -0,0 +1,194 @@
package envoy.client.ui.chatscene;
import java.util.Random;
import java.util.function.*;
import java.util.logging.Level;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.skin.VirtualFlow;
import envoy.data.Message;
import envoy.data.User.UserStatus;
import envoy.util.EnvoyLog;
import envoy.client.data.Context;
import envoy.client.data.commands.*;
import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.ChatScene;
import envoy.client.util.*;
/**
* Contains all {@link SystemCommand}s used for {@link envoy.client.ui.controller.ChatScene}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class ChatSceneCommands {
private final ListView<Message> messageList;
private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
private final SystemCommandBuilder builder =
new SystemCommandBuilder(messageTextAreaCommands);
private static final String messageDependantCommandDescription =
" the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message.";
/**
* @param messageList the message list to use for some commands
* @param chatScene the instance of {@code ChatScene} that uses this object
* @since Envoy Client v0.3-beta
*/
public ChatSceneCommands(ListView<Message> messageList, ChatScene chatScene) {
this.messageList = messageList;
// Error message initialization
builder.setAction(text -> { throw new RuntimeException(); })
.setDescription("Shows an error message.").buildNoArg("error");
// Do A Barrel roll initialization
final var random = new Random();
builder
.setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)),
Double.parseDouble(text.get(1))))
.setDefaults(Integer.toString(random.nextInt(3) + 1),
Double.toString(random.nextDouble() * 3 + 1))
.setDescription("See for yourself :)")
.setNumberOfArguments(2)
.build("dabr");
// Logout initialization
builder.setAction(text -> UserUtil.logout()).setDescription("Logs you out.")
.buildNoArg("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 -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE))
.setDescription("Opens the settings screen")
.buildNoArg("settings");
// Status change initialization
builder.setAction(text -> {
try {
UserUtil.changeStatus(Enum.valueOf(UserStatus.class, text.get(0).toUpperCase()));
} catch (final IllegalArgumentException e) {
final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Please provide an existing status");
alert.showAndWait();
}
}).setDescription("Changes your status to the given status.").setNumberOfArguments(1)
.setDefaults("").build("status");
// Selection of a new message initialization
messageDependantAction("s",
m -> {
messageList.getSelectionModel().clearSelection();
messageList.getSelectionModel().select(m);
},
m -> true,
"Selects");
// Copy text of selection initialization
messageDependantAction("cp", MessageUtil::copyMessageText, m -> !m.getText().isEmpty(),
"Copies the text of");
// Delete selection initialization
messageDependantAction("del", MessageUtil::deleteMessage, m -> true, "Deletes");
// Save attachment of selection initialization
messageDependantAction("save-att", MessageUtil::saveAttachment, Message::hasAttachment,
"Saves the attachment of");
}
private void messageDependantAction(String command, Consumer<Message> action,
Predicate<Message> additionalCheck, String description) {
builder.setAction(text -> {
final var positionalArgument = text.get(0).toLowerCase();
// the currently selected message was requested
if (positionalArgument.startsWith("s")) {
final var relativeString =
positionalArgument.length() == 1 ? "" : positionalArgument.substring(1);
// Only s has been used as input
if (positionalArgument.length() == 1) {
final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
if (selectedMessage != null && additionalCheck.test(selectedMessage))
action.accept(selectedMessage);
return;
// Either s++ or s-- has been requested
} else if (relativeString.equals("++") || relativeString.equals("--"))
selectionNeighbor(action, additionalCheck, positionalArgument);
// A message relative to the currently selected message should be used (i.e.
// s+4)
else
useRelativeMessage(command, action, additionalCheck, relativeString, true);
// Either ++s or --s has been requested
} else if (positionalArgument.equals("--s") || positionalArgument.equals("++s"))
selectionNeighbor(action, additionalCheck, positionalArgument);
// Just a number is expected: ((+)4)
else
useRelativeMessage(command, action, additionalCheck, positionalArgument, false);
}).setDefaults("s").setNumberOfArguments(1)
.setDescription(description.concat(messageDependantCommandDescription)).build(command);
}
private void selectionNeighbor(Consumer<Message> action, Predicate<Message> additionalCheck,
final String positionalArgument) {
final var wantedIndex = messageList.getSelectionModel().getSelectedIndex()
+ (positionalArgument.contains("+") ? 1 : -1);
messageList.getSelectionModel().clearAndSelect(wantedIndex);
final var selectedMessage = messageList.getItems().get(wantedIndex);
if (selectedMessage != null && additionalCheck.test(selectedMessage))
action.accept(selectedMessage);
}
private void useRelativeMessage(String command, Consumer<Message> action,
Predicate<Message> additionalCheck, final String positionalArgument,
boolean useSelectedMessage) throws NumberFormatException {
final var stripPlus =
positionalArgument.startsWith("+") ? positionalArgument.substring(1)
: positionalArgument;
final var incDec = Integer.valueOf(stripPlus);
try {
// The currently selected message is the base message
if (useSelectedMessage) {
final var messageToUse = messageList.getItems()
.get(messageList.getSelectionModel().getSelectedIndex() + incDec);
if (messageToUse != null && additionalCheck.test(messageToUse))
action.accept(messageToUse);
// The currently upmost completely visible message is the base message
} else {
final var messageToUse = messageList.getItems()
.get(((VirtualFlow<?>) messageList.lookup(".virtual-flow"))
.getFirstVisibleCell().getIndex() + 1 + incDec);
if (messageToUse != null && additionalCheck.test(messageToUse))
action.accept(messageToUse);
}
} catch (final IndexOutOfBoundsException e) {
EnvoyLog.getLogger(ChatSceneCommands.class)
.log(Level.INFO,
" A non-existing message was requested by the user for System command "
+ command);
}
}
/**
* @return the map used by this {@code ChatSceneCommands}
* @since Envoy Client v0.3-beta
*/
public SystemCommandMap getChatSceneCommands() { return messageTextAreaCommands; }
}

View File

@ -0,0 +1,107 @@
package envoy.client.ui.chatscene;
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");
/**
* 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());
selectAllMI.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(new SeparatorMenuItem());
getItems().add(cutMI);
getItems().add(copyMI);
getItems().add(pasteMI);
getItems().add(new SeparatorMenuItem());
getItems().add(deleteMI);
getItems().add(clearMI);
getItems().add(new SeparatorMenuItem());
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,7 @@
/**
* Contains classes that influence the appearance and behavior of ChatScene.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
package envoy.client.ui.chatscene;

View File

@ -1,30 +1,25 @@
package envoy.client.ui;
package envoy.client.ui.control;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.*;
import javafx.scene.control.Alert;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import envoy.client.data.audio.AudioPlayer;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.client.data.audio.AudioPlayer;
/**
* 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
* @since Envoy Client v0.1-beta
*/
public final class AudioControl extends HBox {
private AudioPlayer player = new AudioPlayer();
private final AudioPlayer player = new AudioPlayer();
private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);

View File

@ -0,0 +1,63 @@
package envoy.client.ui.control;
import javafx.geometry.*;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.*;
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);
/**
* Creates a new {@code ChatControl}.
*
* @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
var contactProfilePic =
new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32);
getChildren().add(contactProfilePic);
// Spacing
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) {
var spacing = new Region();
setHgrow(spacing, Priority.ALWAYS);
getChildren().add(spacing);
var unreadMessagesLabel = new Label(
chat.getUnreadAmount() > 99 ? "99+" : String.valueOf(chat.getUnreadAmount()));
unreadMessagesLabel.setMinSize(15, 15);
unreadMessagesLabel.setAlignment(Pos.CENTER_RIGHT);
unreadMessagesLabel.getStyleClass().add("unread-messages-amount");
getChildren().add(unreadMessagesLabel);
}
getStyleClass().add("list-element");
}
}

View File

@ -0,0 +1,51 @@
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 {
private final Contact contact;
/**
* @param contact the contact to display
* @since Envoy Client v0.2-beta
*/
public ContactControl(Contact contact) {
this.contact = contact;
// Name label
final var nameLabel = new Label(contact.getName());
getChildren().add(nameLabel);
// Online status (user) or member count (group)
getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact)
: new GroupSizeLabel((Group) contact));
getStyleClass().add("list-element");
}
/**
* Replaces the info label of this {@code ContactControl} with an updated version.
* <p>
* This method should be called when the status of the underlying user or the size of the
* underlying group has changed.
*
* @since Envoy Client v0.3-beta
* @apiNote will produce buggy results if contact control gets updated so that the info label is
* no longer on index 1.
*/
public void replaceInfoLabel() {
getChildren().set(1, contact instanceof User ? new UserStatusLabel((User) contact)
: new GroupSizeLabel((Group) contact));
}
}

View File

@ -0,0 +1,23 @@
package envoy.client.ui.control;
import javafx.scene.control.Label;
import envoy.data.Group;
/**
* Displays the amount of members in a {@link Group}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class GroupSizeLabel extends Label {
/**
* @param recipient the group whose members to show
* @since Envoy Client v0.3-beta
*/
public GroupSizeLabel(Group recipient) {
super(recipient.getContacts().size() + " member"
+ (recipient.getContacts().size() != 1 ? "s" : ""));
}
}

View File

@ -0,0 +1,165 @@
package envoy.client.ui.control;
import java.io.ByteArrayInputStream;
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 envoy.data.*;
import envoy.data.Message.MessageStatus;
import envoy.util.EnvoyLog;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.util.*;
/**
* 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.getLocalDB();
private final Client client = context.getClient();
private static final Context context = Context.getInstance();
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 Logger logger =
EnvoyLog.getLogger(MessageControl.class);
/**
* @param message the message that should be formatted
* @since Envoy Client v0.1-beta
*/
public MessageControl(Message message) {
ownMessage = message.getSenderID() == localDB.getUser().getID();
// 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 items = contextMenu.getItems();
// Copy message action
if (!message.getText().isEmpty()) {
final var copyMenuItem = new MenuItem("Copy Text");
copyMenuItem.setOnAction(e -> MessageUtil.copyMessageText(message));
items.add(copyMenuItem);
}
// Delete message
final var deleteMenuItem = new MenuItem("Delete locally");
deleteMenuItem.setOnAction(e -> MessageUtil.deleteMessage(message));
items.add(deleteMenuItem);
// As long as these types of messages are not implemented and no caches are
// defined for them, we only want them to appear when being online
if (client.isOnline()) {
// Forward menu item
final var forwardMenuItem = new MenuItem("Forward");
forwardMenuItem.setOnAction(e -> MessageUtil.forwardMessage(message));
items.add(forwardMenuItem);
// Quote menu item
final var quoteMenuItem = new MenuItem("Quote");
quoteMenuItem.setOnAction(e -> MessageUtil.quoteMessage(message));
items.add(quoteMenuItem);
}
// Info actions
final var infoMenuItem = new MenuItem("Info");
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
items.add(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 -> MessageUtil.saveAttachment(message));
items.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");
hbox.setAlignment(Pos.CENTER_RIGHT);
} else
getStyleClass().add("received-message");
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);
}
private void loadMessageInfoScene(Message message) {
logger.log(Level.FINEST, "message info scene was requested for " + message);
}
/**
* @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,61 @@
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,94 @@
package envoy.client.ui.control;
import java.util.function.Consumer;
import javafx.geometry.*;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import envoy.data.User;
import envoy.client.util.IconUtil;
/**
* Displays an {@link User} as a quick select control which is used in the quick select list.
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.3-beta
*/
public class QuickSelectControl extends VBox {
private final User user;
/**
* Creates an instance of the {@code QuickSelectControl}.
*
* @param user the contact whose data is used to create this instance.
* @param action the action to perform when a contact is removed with this control as a
* parameter
* @since Envoy Client v0.3-beta
*/
public QuickSelectControl(User user, Consumer<QuickSelectControl> action) {
this.user = user;
setPadding(new Insets(1, 0, 0, 0));
setPrefWidth(37);
setMaxWidth(37);
setMinWidth(37);
var stackPane = new StackPane();
stackPane.setAlignment(Pos.TOP_CENTER);
// Profile picture
var picHold = new VBox();
picHold.setPadding(new Insets(2, 0, 0, 0));
picHold.setPrefHeight(35);
picHold.setMaxHeight(35);
picHold.setMinHeight(35);
var contactProfilePic =
new ImageView(IconUtil.loadIconThemeSensitive("user_icon", 32));
final var clip = new Rectangle();
clip.setWidth(32);
clip.setHeight(32);
clip.setArcHeight(32);
clip.setArcWidth(32);
contactProfilePic.setClip(clip);
picHold.getChildren().add(contactProfilePic);
stackPane.getChildren().add(picHold);
var hBox = new HBox();
hBox.setPrefHeight(12);
hBox.setMaxHeight(12);
hBox.setMinHeight(12);
var region = new Region();
hBox.getChildren().add(region);
HBox.setHgrow(region, Priority.ALWAYS);
var removeBtn = new Button();
removeBtn.setPrefSize(12, 12);
removeBtn.setMaxSize(12, 12);
removeBtn.setMinSize(12, 12);
removeBtn.setOnMouseClicked(evt -> action.accept(this));
removeBtn.setId("remove-button");
hBox.getChildren().add(removeBtn);
stackPane.getChildren().add(hBox);
getChildren().add(stackPane);
var nameLabel = new Label();
nameLabel.setPrefSize(35, 20);
nameLabel.setMaxSize(35, 20);
nameLabel.setMinSize(35, 20);
nameLabel.setText(user.getName());
nameLabel.setAlignment(Pos.TOP_CENTER);
nameLabel.setPadding(new Insets(0, 5, 0, 0));
getChildren().add(nameLabel);
getStyleClass().add("quick-select");
}
/**
* @return the user whose data is used in this instance
* @since Envoy Client v0.3-beta
*/
public User getUser() { return user; }
}

View File

@ -0,0 +1,23 @@
package envoy.client.ui.control;
import javafx.scene.control.Label;
import envoy.data.User;
/**
* Displays the status of a {@link User}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class UserStatusLabel extends Label {
/**
* @param user the user whose status to display
* @since Envoy Client v0.3-beta
*/
public UserStatusLabel(User user) {
super(user.getStatus().toString());
getStyleClass().add(user.getStatus().toString().toLowerCase());
}
}

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

@ -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,139 @@
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 dev.kske.eventbus.*;
import envoy.data.User;
import envoy.event.ElementOperation;
import envoy.event.contact.*;
import envoy.util.EnvoyLog;
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;
/**
* 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 onUserOperation(UserOperation 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 UserOperation} 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 UserOperation(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() {
searchBar.setText("");
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,255 @@
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.input.MouseEvent;
import javafx.scene.layout.HBox;
import dev.kske.eventbus.*;
import envoy.data.*;
import envoy.event.GroupCreation;
import envoy.event.contact.UserOperation;
import envoy.util.Bounds;
import envoy.client.data.*;
import envoy.client.event.BackEvent;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.ListCellFactory;
/**
* 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;
@FXML
private ListView<QuickSelectControl> quickSelectList;
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));
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()));
resizeQuickSelectSpace(0);
quickSelectList.addEventFilter(MouseEvent.MOUSE_PRESSED, MouseEvent::consume);
}
/**
* Enables the {@code createButton} if at least one contact is selected.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void userListClicked() {
if (userList.getSelectionModel().getSelectedItem() != null) {
quickSelectList.getItems().add(new QuickSelectControl(
userList.getSelectionModel().getSelectedItem(), this::removeFromQuickSelection));
createButton.setDisable(
quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank());
resizeQuickSelectSpace(60);
userList.getItems().remove(userList.getSelectionModel().getSelectedItem());
userList.getSelectionModel().clearSelection();
}
}
/**
* 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(quickSelectList.getItems().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();
quickSelectList.getItems().forEach(q -> userList.getItems().add(q.getUser()));
quickSelectList.getItems().clear();
resizeQuickSelectSpace(0);
}
}
/**
* 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, quickSelectList.getItems().stream()
.map(q -> q.getUser().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);
}
/**
* Removes an element from the quickSelectList.
*
* @param element the element to be removed.
* @since Envoy Client v0.3-beta
*/
public void removeFromQuickSelection(QuickSelectControl element) {
quickSelectList.getItems().remove(element);
userList.getItems().add(element.getUser());
if (quickSelectList.getItems().isEmpty()) {
resizeQuickSelectSpace(0);
createButton.setDisable(true);
}
}
private void resizeQuickSelectSpace(int value) {
quickSelectList.setPrefHeight(value);
quickSelectList.setMaxHeight(value);
quickSelectList.setMinHeight(value);
}
@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 onUserOperation(UserOperation operation) {
Platform.runLater(() -> {
switch (operation.getOperationType()) {
case ADD:
userList.getItems().add(operation.get());
break;
case REMOVE:
userList.getItems().removeIf(operation.get()::equals);
break;
}
});
}
}

View File

@ -1,44 +1,36 @@
package envoy.client.ui.controller;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.time.Instant;
import java.util.logging.*;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.ImageView;
import dev.kske.eventbus.*;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.Startup;
import envoy.data.LoginCredentials;
import envoy.data.User;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import envoy.event.HandshakeRejection;
import envoy.exception.EnvoyException;
import envoy.util.Bounds;
import envoy.util.EnvoyLog;
import envoy.util.*;
import envoy.client.data.ClientConfig;
import envoy.client.ui.Startup;
import envoy.client.util.IconUtil;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>LoginDialog.java</strong><br>
* Created: <strong>03.04.2020</strong><br>
* Controller for the login scene.
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public final class LoginScene {
public final class LoginScene implements EventListener {
@FXML
private ClearableTextField userTextField;
private TextField userTextField;
@FXML
private PasswordField passwordField;
@ -47,81 +39,93 @@ public final class LoginScene {
private PasswordField repeatPasswordField;
@FXML
private Label repeatPasswordLabel;
@FXML
private CheckBox registerCheckBox;
private Button registerSwitch;
@FXML
private Label connectionLabel;
private Client client;
private LocalDB localDB;
private CacheMap cacheMap;
private SceneContext sceneContext;
@FXML
private Button loginButton;
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
private static final EventBus eventBus = EventBus.getInstance();
private static final ClientConfig config = ClientConfig.getInstance();
@FXML
private CheckBox cbStaySignedIn;
@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
private void initialize() {
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
// 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);
/**
* 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();
logo.setImage(IconUtil.loadIcon("envoy_logo"));
// Set initial cursor
userTextField.requestFocus();
// Perform automatic login if configured
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
}
@FXML
private void loginButtonPressed() {
final String user = userTextField.getText(), pass = passwordField.getText(),
repeatPass = repeatPasswordField.getText();
final boolean requestToken = cbStaySignedIn.isSelected();
// Prevent registration with unequal passwords
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
if (registration && !pass.equals(repeatPass)) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one")
.showAndWait();
repeatPasswordField.clear();
} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) {
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
userTextField.getTextField().clear();
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
Startup.VERSION));
} else if (!Bounds.isValidContactName(user)) {
new Alert(AlertType.ERROR,
"The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")")
.showAndWait();
userTextField.clear();
} else {
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
private void offlineModeButtonPressed() {
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
Startup.attemptOfflineMode(userTextField.getText());
}
@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
repeatPasswordField.setVisible(registerCheckBox.isSelected());
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
repeatPasswordField.setVisible(registration);
offlineModeButton.setDisable(registration);
}
@FXML
@ -130,70 +134,8 @@ public final class LoginScene {
System.exit(0);
}
private void performHandshake(LoginCredentials credentials) {
try {
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);
@Event
private void onHandshakeRejection(HandshakeRejection evt) {
Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait());
}
}

View File

@ -1,21 +1,23 @@
package envoy.client.ui.controller;
import java.util.Map;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.input.*;
import envoy.client.ui.SceneContext;
import envoy.client.ui.settings.GeneralSettingsPane;
import envoy.client.ui.settings.SettingsPane;
import envoy.client.data.Context;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.ui.listcell.ListCellFactory;
import envoy.client.ui.settings.*;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsSceneController.java</strong><br>
* Created: <strong>10.04.2020</strong><br>
* Controller for the settings scene.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class SettingsScene {
public final class SettingsScene implements KeyboardMapping {
@FXML
private ListView<SettingsPane> settingsList;
@ -23,26 +25,11 @@ public class SettingsScene {
@FXML
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
private void initialize() {
settingsList.setCellFactory(listView -> new ListCell<>() {
@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());
settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle())));
settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(),
new DownloadSettingsPane(), new BugReportPane());
}
@FXML
@ -55,5 +42,13 @@ public class SettingsScene {
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
private void backButtonClicked() {
Context.getInstance().getSceneContext().pop();
}
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.of(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN),
this::backButtonClicked);
}
}

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.
* <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 Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui.controller;

View File

@ -0,0 +1,48 @@
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);
setCursor(Cursor.DEFAULT);
}
}
/**
* 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

@ -0,0 +1,55 @@
package envoy.client.ui.listcell;
import javafx.scene.control.*;
import envoy.data.User;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.ui.control.ChatControl;
import envoy.client.util.UserUtil;
/**
* A list cell containing chats represented as chat controls.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class ChatListCell extends AbstractListCell<Chat, ChatControl> {
private static final Client client = Context.getInstance().getClient();
/**
* @param listView the list view inside of which the cell will be displayed
* @since Envoy Client v0.3-beta
*/
public ChatListCell(ListView<? extends Chat> listView) {
super(listView);
}
@Override
protected ChatControl renderItem(Chat chat) {
if (client.isOnline()) {
final var menu = new ContextMenu();
final var removeMI = new MenuItem();
removeMI.setText(
chat.isDisabled() ? "Delete "
: chat.getRecipient() instanceof User ? "Block "
: "Leave group " + chat.getRecipient().getName());
removeMI.setOnAction(
chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient())
: e -> UserUtil.disableContact(chat.getRecipient()));
menu.getItems().add(removeMI);
setContextMenu(menu);
} else
setContextMenu(null);
// TODO: replace with icon in ChatControl
final var chatControl = new ChatControl(chat);
if (chat.isDisabled())
chatControl.getStyleClass().add("disabled-chat");
else
chatControl.getStyleClass().remove("disabled-chat");
return chatControl;
}
}

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,34 @@
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,34 @@
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,43 @@
package envoy.client.ui.listcell;
import javafx.geometry.*;
import javafx.scene.control.ListView;
import envoy.data.Message;
import envoy.client.ui.control.MessageControl;
/**
* 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(3, padding, 3, 0) : new Insets(3, 0, 3, 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,8 @@
/**
* This package contains custom list cells that are used to display certain
* things.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>30.06.2020</strong><br>
* This package contains custom list cells that are used to display certain things.
*
* @author Leon Hofmeister
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui.listcell;

View File

@ -0,0 +1,71 @@
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,63 @@
package envoy.client.ui.settings;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.stage.DirectoryChooser;
/**
* 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.getSceneContext().getStage());
if (selectedDirectory != null) {
currentPath.setText(selectedDirectory.getAbsolutePath());
settings.setDownloadLocation(selectedDirectory);
}
});
hbox.getChildren().add(button);
getChildren().add(hbox);
}
}

View File

@ -1,57 +1,83 @@
package envoy.client.ui.settings;
import java.util.List;
import javafx.scene.control.*;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import dev.kske.eventbus.EventBus;
import envoy.data.User.UserStatus;
import envoy.client.data.Settings;
import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import envoy.client.ui.StatusTrayIcon;
import envoy.client.util.UserUtil;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>GeneralSettingsPane.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class GeneralSettingsPane extends SettingsPane {
private static final Settings settings = Settings.getInstance();
public final class GeneralSettingsPane extends SettingsPane {
/**
* @since Envoy Client v0.1-beta
*/
public GeneralSettingsPane() {
super("General");
final var vbox = new VBox();
setSpacing(10);
// TODO: Support other value types
List.of("onCloseMode", "enterToSend")
.stream()
.map(settings.getItems()::get)
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
.forEach(vbox.getChildren()::add);
final var settingsItems = settings.getItems();
// Add hide on close if supported
final var hideOnCloseCheckbox =
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
final var hideOnCloseTooltip = new Tooltip(StatusTrayIcon.isSupported()
? "If selected, Envoy will still be present in the task bar when closed."
: "status tray icon is not supported on your system.");
hideOnCloseTooltip.setWrapText(true);
hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
hideOnCloseCheckbox.setDisable(!StatusTrayIcon.isSupported());
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>();
combobox.getItems().add("dark");
combobox.getItems().add("light");
combobox
.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction(
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); });
vbox.getChildren().add(combobox);
combobox.setOnAction(e -> {
settings.setCurrentTheme(combobox.getValue());
EventBus.getInstance().dispatch(new ThemeChangeEvent());
});
getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>();
statusComboBox.getItems().setAll(UserStatus.values());
statusComboBox.setValue(UserStatus.ONLINE);
// TODO add action when value is changed
statusComboBox.setOnAction(e -> {});
vbox.getChildren().add(statusComboBox);
statusComboBox.setValue(context.getLocalDB().getUser().getStatus());
statusComboBox.setTooltip(new Tooltip("Change your current status"));
statusComboBox.setOnAction(e -> UserUtil.changeStatus(statusComboBox.getValue()));
getChildren().add(statusComboBox);
getChildren().add(vbox);
final var logoutButton = new Button("Logout");
logoutButton.setOnAction(e -> UserUtil.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);
}
}

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