Compare commits

...

1601 Commits

Author SHA1 Message Date
Vitor Pamplona
78d2a712c5 v0.70.4 2023-07-21 17:29:51 -04:00
Vitor Pamplona
2664292993 Moves wake lock to activate only when the video is actually playing 2023-07-21 17:08:56 -04:00
Vitor Pamplona
b9813c2c8f v0.70.3 2023-07-21 11:48:03 -04:00
Vitor Pamplona
0c00e6512f Modifier refactoring 2023-07-21 11:47:28 -04:00
Vitor Pamplona
cf31bfe5ea Adds a space when rendering inline images 2023-07-21 11:47:08 -04:00
Vitor Pamplona
5cdceb5194 Moves the loading of an Accounts backup contacts to the IO Thread 2023-07-21 11:46:31 -04:00
Vitor Pamplona
2c82e6c447 Adjustments to Modifiers in the Chatroom screen 2023-07-21 11:21:07 -04:00
Vitor Pamplona
f56f3b6105 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-07-21 11:16:55 -04:00
Vitor Pamplona
9420ddc5c7 Merge pull request #517 from greenart7c3/main
fix KeepPlayingButton color in light theme
2023-07-21 11:16:41 -04:00
greenart7c3
ade1d83817 fix KeepPlayingButton color in light theme 2023-07-21 12:11:23 -03:00
Vitor Pamplona
9a517380a0 Refactors ChatroomHeader compose 2023-07-21 11:02:16 -04:00
Vitor Pamplona
6fbb59ddac v0.70.2 2023-07-21 10:44:43 -04:00
Vitor Pamplona
fe4a4b6fad Stops Video playback when switching in and out of Tor 2023-07-21 10:44:02 -04:00
Vitor Pamplona
8dd5705f02 Merges VideoCache initialization with get methods 2023-07-21 10:08:04 -04:00
Vitor Pamplona
6723225885 Creates playback managers in a lazy mode, after the initialization of the video caching service. 2023-07-21 09:48:25 -04:00
Vitor Pamplona
fb19454259 Moves the video caching service initialization foreground services. 2023-07-21 09:47:54 -04:00
Vitor Pamplona
c6f63a13d3 Merge pull request #511 from greenart7c3/main
add option to send notes to selected relays
2023-07-21 09:06:44 -04:00
Vitor Pamplona
77057fcac5 Apply hidden to boosted notes only. 2023-07-21 09:05:13 -04:00
Vitor Pamplona
676d5e9b88 Merge pull request #516 from greenart7c3/boosted_blocked
fix boosted notes from blocked users appearing as blank
2023-07-21 08:58:05 -04:00
greenart7c3
d6dda0053a fix boosted notes from blocked users appearing as blank 2023-07-21 05:41:50 -03:00
greenart7c3
d33e62367e Merge branch 'main' into main 2023-07-21 05:24:12 -03:00
Vitor Pamplona
47d7a53c1c Merge pull request #513 from davotoula/update_translations
Updated translations
2023-07-20 20:03:22 -04:00
Vitor Pamplona
6fcf472638 Merge branch 'main' into update_translations 2023-07-20 20:03:17 -04:00
Vitor Pamplona
e2f5a1a0e4 v0.70.1 2023-07-20 19:48:02 -04:00
Vitor Pamplona
8ea25e735d Fixes wakelock permission for the f-droid build 2023-07-20 19:46:55 -04:00
Vitor Pamplona
a9d05070ed Remove restrictions of height in the first and second rows of the note. 2023-07-20 19:35:16 -04:00
Vitor Pamplona
d78ec6b02a Centers the tag list vertically 2023-07-20 19:34:52 -04:00
Vitor Pamplona
8971b4e190 Samsung phones don't seem to have a . after hr and min 2023-07-20 19:34:33 -04:00
Vitor Pamplona
dea0adaea4 Reducing the distance from boost to the play button 2023-07-20 19:34:12 -04:00
Vitor Pamplona
5de4fbcbe7 Fixing the size of the 3 dot menu in post 2023-07-20 19:33:54 -04:00
Vitor Pamplona
2330e086ea v0.70.0 2023-07-20 18:02:01 -04:00
Vitor Pamplona
11d51b3854 Wrong Immutable Class 2023-07-20 18:01:17 -04:00
Vitor Pamplona
008469adbc Background Playback
- Moves Video/Audio player to a foreground service.
- Migrates Feed, Stories and Live Stream screens to that service
- Blocks screen from going to sleep if a video is playing.
- Blocks WIFI from going to sleep if an online video is playing.
- Allows the app to pause while listening to media
- Manages cache for up to 30 videos in parallel for each of the 3 categories: local, streaming, progressive content
- Activates the use of popup with artwork that points to the screen with the video
- Creates a button to allow any video to play while browsing the app/phone
- Moves app to SingleTop mode.
- Keeps viewed position cached for up to 100 videos.
- Restructures the starting screen from App Nativation
2023-07-20 17:59:10 -04:00
David Kaspar
bbe26c7a30 Updated se/de/cs translations
Added missing DE locale config
2023-07-18 20:29:19 +02:00
greenart7c3
c2b9bb413d add long press to show relay info, fix validation, show relay url without prefix 2023-07-17 17:08:14 -03:00
greenart7c3
5c1c4f8db2 Merge branch 'main' of github.com:greenart7c3/amethyst 2023-07-17 15:44:33 -03:00
greenart7c3
b46bb7ecc5 add relay selection to nip 94 and 95 uploads 2023-07-17 15:43:49 -03:00
greenart7c3
2fed65db8d created RelaySelectionDialog file 2023-07-17 15:43:49 -03:00
greenart7c3
66cc4b5526 remove unused import 2023-07-17 15:43:49 -03:00
greenart7c3
91488f0350 add option to send notes to selected relays 2023-07-17 15:43:49 -03:00
Vitor Pamplona
f2429e011e Upgrades the compressor API to the new version 2023-07-17 14:28:52 -04:00
Vitor Pamplona
1374f36b1f Updating libraries 2023-07-17 13:42:17 -04:00
Vitor Pamplona
d185e65b3f Fixes Like icon aligned to the left in the Notifcations screen 2023-07-17 13:42:08 -04:00
Vitor Pamplona
64bf17b1a0 v0.69.3 2023-07-17 12:16:40 -04:00
Vitor Pamplona
93b50468fd Adjusts colors for the NÏP05 to be more subtle. 2023-07-17 12:14:21 -04:00
Vitor Pamplona
65ab756c2b v0.69.2 2023-07-17 11:50:23 -04:00
Vitor Pamplona
e298285145 Migration to Niel Liesmons' icons 2023-07-17 11:48:29 -04:00
greenart7c3
d730eeb9a1 add relay selection to nip 94 and 95 uploads 2023-07-17 11:15:19 -03:00
greenart7c3
0d63441611 created RelaySelectionDialog file 2023-07-17 10:41:47 -03:00
greenart7c3
f0ecf382e0 remove unused import 2023-07-17 09:56:16 -03:00
greenart7c3
d0dee84031 add option to send notes to selected relays 2023-07-17 09:48:34 -03:00
Vitor Pamplona
ad59b691fd v0.69.1 2023-07-16 20:17:53 -04:00
Vitor Pamplona
a256e34878 Fix for mute list not showing 2023-07-16 20:16:17 -04:00
Vitor Pamplona
6ad00c112a v0.69.0 2023-07-16 19:07:28 -04:00
Vitor Pamplona
0010a19c24 Fixes the profile display for new users without pictures. 2023-07-16 18:57:48 -04:00
Vitor Pamplona
d78c7a91cb Lot's of changes from local storage to relay storage.
- Moves Following Communities local DB to Contact List
- Moves Following Channels local DB to Contact List
- Moves Following BlockList local DB to Mute List (Private part)
- Migrates all deprecated local lists to event kinds.
- Views Mute Feed (disables hidden authors for that specific list)
- Breaks Security Filters screen in 2 tabs: Blocked and Spammers (automated filter)
- Restructures PeopleList event kind
- Fixes older channels and communities not loading on the discovery tab
2023-07-16 18:57:28 -04:00
Vitor Pamplona
4a2dced02f Formatting 2023-07-16 11:53:12 -04:00
Vitor Pamplona
2d1a63be57 Looks like relays accept id prefixes of 8 more frequently than 6 2023-07-16 11:50:55 -04:00
Vitor Pamplona
9e207d0f24 Adds parsing support for NNS Events. 2023-07-16 11:50:16 -04:00
Vitor Pamplona
efad94ddfd Moves contact list to closer to metadata in the Local Cache 2023-07-16 11:49:41 -04:00
Vitor Pamplona
d49fde755e BugFix for AppDefinitionEvent updates 2023-07-16 11:49:15 -04:00
Vitor Pamplona
0224af18c0 Forces a given event kind to be displayed in the Discovery tab 2023-07-16 11:47:05 -04:00
Vitor Pamplona
53a4dfb702 Moves video player to the new Exoplayer package 2023-07-16 11:42:55 -04:00
Vitor Pamplona
4e4e798133 v0.68.4 2023-07-14 18:58:45 -04:00
Vitor Pamplona
e8bd4a12ac Reduces profile feed size to 200 and zaps, reports and followers to 400 2023-07-14 18:55:57 -04:00
Vitor Pamplona
dffed6e78f Moves synchronized Zap allocation to after checking if the zap already exists for this user. 2023-07-14 18:52:11 -04:00
Vitor Pamplona
46a4f360f7 Moves UserProfile NewThreads, Conversations, Reports and App Recommendations to be additive filters 2023-07-14 18:22:53 -04:00
Vitor Pamplona
64a5071345 Moves NIP-05 captions from NIP-05 to Nostr Address 2023-07-14 17:41:16 -04:00
Vitor Pamplona
1fba93b4f0 Moves badges out of the User class 2023-07-14 17:19:40 -04:00
Vitor Pamplona
caa67b03a8 Keeping media feed active from the start of the app. 2023-07-14 16:54:57 -04:00
Vitor Pamplona
5f1b2f9eb8 Only logging pruning and printing stacktraces when it matters 2023-07-14 16:54:43 -04:00
Vitor Pamplona
2968cb12aa Adds EOSE limits to the Video tab 2023-07-14 16:31:57 -04:00
Vitor Pamplona
8261b38de9 Adds EOSE limits to the Discovery tab. 2023-07-14 16:31:48 -04:00
Vitor Pamplona
2c4489a0d0 Only triggers mutable state if the state actually changes. 2023-07-14 16:31:31 -04:00
Vitor Pamplona
52dcb73fd7 Puts the Wifi Processing in an IO Thread 2023-07-14 16:30:48 -04:00
Vitor Pamplona
91a2f8ecfa v0.68.3 2023-07-14 09:33:08 -04:00
Vitor Pamplona
6905e1b47f Clicks on Community and Channel headers now take you to the community instead of expanding 2023-07-14 09:22:56 -04:00
Vitor Pamplona
640b0d3174 Adds the post highlighted to the highlight display 2023-07-14 09:04:34 -04:00
Vitor Pamplona
a86cd8772f Merge remote-tracking branch 'origin/HEAD' 2023-07-14 08:38:49 -04:00
Vitor Pamplona
83be195a43 Migrates DropDownMenu to MutableState 2023-07-14 08:38:34 -04:00
Vitor Pamplona
14bf73e2f1 Merge pull request #504 from akiomik/update-i18n-ja
Update translations for ja
2023-07-14 07:43:18 -04:00
Vitor Pamplona
efb7b06e4e Merge pull request #505 from greenart7c3/main
fix crash when changing language in android < 13
2023-07-14 07:42:58 -04:00
Vitor Pamplona
8b9f37d678 Merge pull request #506 from greenart7c3/string_resources
update pt-br translations
2023-07-14 07:42:29 -04:00
greenart7c3
e83f16f5c5 update pt-br translations 2023-07-14 06:46:30 -03:00
greenart7c3
e648970b17 fix crash when changing setting in android api < 13 2023-07-14 05:51:09 -03:00
Akiomi Kamakura
44badf8c86 Update translations for ja 2023-07-14 15:21:13 +09:00
Vitor Pamplona
516ff17eed Activates NostrCheck on the Media Feed 2023-07-13 21:32:48 -04:00
Vitor Pamplona
3b1533eb82 Fixes size of custom reactions on the Stories feed. 2023-07-13 21:19:08 -04:00
Vitor Pamplona
2e7a955ada v0.68.2 2023-07-13 19:30:52 -04:00
Vitor Pamplona
4aa961437b formatting 2023-07-13 19:23:37 -04:00
Vitor Pamplona
0d9399a1a4 Fixing Right To Left Text 2023-07-13 19:23:26 -04:00
Vitor Pamplona
9b62cf563a Merge pull request #502 from greenart7c3/main
Aways show images/videos in the dialog screen
2023-07-13 18:04:20 -04:00
greenart7c3
7c525e2f2f show videos as links 2023-07-13 18:07:57 -03:00
greenart7c3
63888d55fd fix image loading in ZoomableImageDialog 2023-07-13 17:57:54 -03:00
Vitor Pamplona
7287ead851 v0.68.1 2023-07-13 13:17:20 -04:00
Vitor Pamplona
6fc829f4f1 Merge pull request #486 from greenart7c3/load_media
Optional media pre-loading
2023-07-13 13:14:18 -04:00
Vitor Pamplona
d12b0ce9be Merge pull request #500 from akiomik/fix-string-resource-mapping
Fix string resource mapping when like clicked
2023-07-13 12:56:00 -04:00
Vitor Pamplona
393714eb42 Merge pull request #499 from akiomik/extract-block-report-resource
Extract string resource for Block / Report
2023-07-13 12:55:33 -04:00
Vitor Pamplona
fe363b8a2d Updates readme 2023-07-13 11:27:52 -04:00
Vitor Pamplona
064ce7e6e6 v0.68.0 2023-07-13 11:26:35 -04:00
Vitor Pamplona
af72846998 Support for Classifieds 2023-07-13 11:20:03 -04:00
Vitor Pamplona
899b4f7c90 Quick fix for multiple emoji's in one line 2023-07-13 11:19:07 -04:00
Akiomi Kamakura
2e5b41b711 Add ja to locales_config.xml 2023-07-13 22:57:59 +09:00
Vitor Pamplona
809f048b44 Merge remote-tracking branch 'origin/HEAD' 2023-07-13 08:52:58 -04:00
Vitor Pamplona
51c0c44a2e Formatting 2023-07-13 08:52:47 -04:00
Vitor Pamplona
811d6d8c67 Removes emoji package list selection from Profile threads. 2023-07-13 08:51:54 -04:00
Akiomi Kamakura
c1b9bf42ad Fix string resource mapping when like clicked 2023-07-13 11:05:17 +09:00
Akiomi Kamakura
cafe742cd8 Extract string resource for Block / Report 2023-07-13 11:00:15 +09:00
Vitor Pamplona
ba50ec052b Merge pull request #498 from akiomik/i18n-ja
Add i18n support for ja
2023-07-12 19:59:31 -04:00
Vitor Pamplona
386f97abf8 Updates readme 2023-07-12 19:58:57 -04:00
Akiomi Kamakura
7678b1b100 Add i18n support for ja
Co-authored-by: h3y6e <h3y6e@pm.me>
Co-authored-by: ys3669 <ys3669jp@gmail.com>
2023-07-13 07:51:49 +09:00
Vitor Pamplona
9df82a78e7 v0.67.1 2023-07-12 18:19:47 -04:00
Vitor Pamplona
c87466ada4 when clicking in a repost of a note, redirects the user to the note directly. 2023-07-12 18:19:40 -04:00
Vitor Pamplona
135eb6763a Adjustments to allow removing of the selected emoji packs from the reaction setup 2023-07-12 18:19:20 -04:00
Vitor Pamplona
24a0be9c3a v0.66.7 2023-07-12 17:21:49 -04:00
Vitor Pamplona
f090bc82ae - Support for Emoji Packs
- Support for Personal Emoji Lists
- Support for Custom emoji Reactions
2023-07-12 17:20:06 -04:00
greenart7c3
3254479ee5 add settings icon 2023-07-12 11:41:37 -03:00
greenart7c3
6cc4266435 appcompat needed for language picker and theme 2023-07-12 11:31:13 -03:00
greenart7c3
3e48b293b0 create ThemeViewModel using viewModel contructor 2023-07-12 11:31:13 -03:00
greenart7c3
492b9494ec fix theme not changing after changing language 2023-07-12 11:31:12 -03:00
greenart7c3
b31d2d49d6 do not load preview of long form text if automaticallyShowUrlPreview is false 2023-07-12 11:31:12 -03:00
greenart7c3
245bd4a44d fix lint 2023-07-12 11:31:12 -03:00
greenart7c3
6d4a887d00 use accountviewmodel as parameter instead of MutableState 2023-07-12 11:31:12 -03:00
greenart7c3
f95023f59d replace imageBox with url link with download button 2023-07-12 11:31:12 -03:00
greenart7c3
e69c1cf3a9 remove unused showImage parameters 2023-07-12 11:31:12 -03:00
greenart7c3
b5229f5fe1 only load one image when theres multiple images in the note 2023-07-12 11:31:12 -03:00
greenart7c3
e2ef7cfd70 add string resources 2023-07-12 11:31:12 -03:00
greenart7c3
6e89e69fb6 refactor settings to use TextSpinner and save settings when selecting an option 2023-07-12 11:31:11 -03:00
greenart7c3
1e6ee67932 move back settings to drawer 2023-07-12 11:31:11 -03:00
greenart7c3
0fead83279 fix load image box size 2023-07-12 11:31:11 -03:00
greenart7c3
66952c9212 add url preview settings 2023-07-12 11:31:11 -03:00
greenart7c3
38bb9f457d fix statusbar color when changing theme 2023-07-12 11:31:11 -03:00
greenart7c3
3e615bb152 check for automaticallyStartPlayback setting 2023-07-12 11:31:11 -03:00
greenart7c3
f83a19260f fix build after rebase 2023-07-12 11:31:11 -03:00
greenart7c3
69c3c41196 move drawer options to settings screen 2023-07-12 11:31:11 -03:00
greenart7c3
6c1a4f3971 replace load image button with a rectangle box 2023-07-12 11:31:10 -03:00
greenart7c3
035ba899ee add language picker 2023-07-12 11:31:10 -03:00
greenart7c3
33cc261b62 add settings to change theme 2023-07-12 11:31:10 -03:00
greenart7c3
fee6f19d5e check for load images option 2023-07-12 11:31:10 -03:00
greenart7c3
68e8a13f73 add options to disable auto playback e show images in the settings screen 2023-07-12 11:31:10 -03:00
greenart7c3
a639c3f36d add media settings 2023-07-12 11:31:10 -03:00
greenart7c3
a5f10fef60 created settings screen 2023-07-12 11:31:10 -03:00
greenart7c3
d334c04c47 do not autoplay videos when on mobile data 2023-07-12 11:31:09 -03:00
greenart7c3
7c3d9aad1d add ConnectivityStatus singleton 2023-07-12 11:31:09 -03:00
greenart7c3
ceb81aa2bf created button to load images] 2023-07-12 11:31:09 -03:00
Vitor Pamplona
3ee743ce1a v0.66.7 2023-07-12 10:21:02 -04:00
Vitor Pamplona
d1ffea5432 BugFix for a race condition when deleting events. 2023-07-12 10:20:24 -04:00
Vitor Pamplona
628e73ef69 BugFix for app crashing when the ln invoice is blank 2023-07-12 10:06:27 -04:00
Vitor Pamplona
764928a492 Displays custom emoji reactions 2023-07-12 08:21:10 -04:00
Vitor Pamplona
88638b0cae Fixes the markdown issue with images overlapping the text 2023-07-12 08:16:05 -04:00
Vitor Pamplona
51384e654b Avoiding too much detail in the Log 2023-07-11 17:45:38 -04:00
Vitor Pamplona
aecfaa5fd8 Adds more checks for work in the main thread 2023-07-10 17:22:04 -04:00
Vitor Pamplona
fc5089f7f1 Updates exoplayer version 2023-07-10 14:28:42 -04:00
Vitor Pamplona
25520e7d2a Making nav(route) threaded operation 2023-07-10 14:13:27 -04:00
Vitor Pamplona
7ea5be0152 Refactoring of Badge Box codes and Time classes 2023-07-10 13:50:49 -04:00
Vitor Pamplona
9a7e678bbe Merge pull request #496 from AutumnSunshine/main
Update strings.xml for Tamil
2023-07-09 19:00:17 -04:00
Vitor Pamplona
1f0ca70c47 Adds rendering of approval posts to the NoteMaster of a thread 2023-07-09 18:58:30 -04:00
Vitor Pamplona
93705e5baf v0.66.6 2023-07-09 12:46:56 -04:00
Vitor Pamplona
72e8fa3679 Moves the toggle to only be active in NIP94 and NIP95 images 2023-07-09 12:46:02 -04:00
Vitor Pamplona
c08ce91197 v0.66.5 2023-07-09 12:11:18 -04:00
Vitor Pamplona
b520a0faed Adds sensitive-content tags to NIP94 and NIP95 media 2023-07-09 12:09:21 -04:00
AutumnSunshine
80837abab5 Update strings.xml for Tamil
Added the communities & group translations for Tamil
2023-07-09 11:44:48 +05:30
Vitor Pamplona
5c78135f1a v0.66.4 2023-07-08 17:05:24 -04:00
Vitor Pamplona
386658ed37 Updates vico version 2023-07-08 17:04:59 -04:00
Vitor Pamplona
ab8b0beb02 Making sure multi-threading doesn't duplicate notes in the screen. 2023-07-08 16:35:07 -04:00
Vitor Pamplona
bc89062681 Prints Debug information when the app pauses. 2023-07-08 15:46:46 -04:00
Vitor Pamplona
14685be92d Adds limits to how many messages referencing an event can be downloaded. 2023-07-08 15:46:31 -04:00
Vitor Pamplona
6d565a2960 Simplifies the work of finding an event by relays. 2023-07-08 15:46:02 -04:00
Vitor Pamplona
3fc247d85c Caches participant counting process to speed sorting up 2023-07-08 15:37:05 -04:00
Vitor Pamplona
cfbc4536c6 v0.66.3 2023-07-07 17:15:02 -04:00
Vitor Pamplona
dcf825c43e Slightly faster reactions and zap icons 2023-07-07 17:13:20 -04:00
Vitor Pamplona
8ae05d9964 Formatting 2023-07-07 17:11:47 -04:00
Vitor Pamplona
f11faa87b1 Fix from full height to full screen 2023-07-07 17:09:42 -04:00
Vitor Pamplona
bfbaabd7e8 Fixes translations from Japanese with urls breaking the url 2023-07-07 15:30:51 -04:00
Vitor Pamplona
d35d6a44fe Adding content-sensitivity warnings for LiveStreams 2023-07-07 15:17:20 -04:00
Vitor Pamplona
9c7265565e Avoids triggering the Notification bubble when the live event is simply updated. 2023-07-07 14:40:07 -04:00
Vitor Pamplona
293b023add - Adds hashtags to the description of communities and channels
- Displays descriptions with Translations.
2023-07-07 14:31:04 -04:00
Vitor Pamplona
bee5c1bfa6 v0.66.2 2023-07-07 12:29:22 -04:00
Vitor Pamplona
c93a6dffc4 Creates a fallback for Android9 on OPPO, which doesn't seem able to parse the Japanese regex. 2023-07-07 12:27:57 -04:00
Vitor Pamplona
60dadc0088 BugFix for invalid URLs crashing the app. 2023-07-07 12:15:28 -04:00
Vitor Pamplona
a26c7e3d97 Fixes caching of time display. 2023-07-07 09:45:21 -04:00
Vitor Pamplona
9e2b2cedd6 v0.66.1 2023-07-06 21:10:44 -04:00
Vitor Pamplona
636d59659d Activates reactions and zaps to the channel and community headers. 2023-07-06 21:04:07 -04:00
Vitor Pamplona
63e8391893 Allows notification to the channel headers 2023-07-06 21:03:51 -04:00
Vitor Pamplona
2f354e4b81 Adds EOSE treatment for the Discovery datasource 2023-07-06 21:03:36 -04:00
Vitor Pamplona
b3a369d91b Removes top bar for Community and Hashtag posts 2023-07-06 21:03:05 -04:00
Vitor Pamplona
4406532994 Loads the last 50 messages per following chat. 2023-07-06 21:02:47 -04:00
Vitor Pamplona
8fd5176e22 Makes sure all services stop when the app pauses. 2023-07-06 21:02:18 -04:00
Vitor Pamplona
e3396065c6 Reducing size of the discovery tab strings. 2023-07-06 17:38:55 -04:00
Vitor Pamplona
84600c016e Fixing for emoji families 2023-07-06 17:36:11 -04:00
Vitor Pamplona
64d96ab1d7 Fixes Emoji Joining issues. 2023-07-06 17:24:42 -04:00
Vitor Pamplona
6d6720a511 Adds reactions to Chats and Communities 2023-07-06 15:51:46 -04:00
Vitor Pamplona
17232a1909 Moves communities from /c/name to /n/name 2023-07-06 15:51:28 -04:00
Vitor Pamplona
eb02049507 Turns parsing failures into warnings. 2023-07-06 15:51:14 -04:00
Vitor Pamplona
ed7ad2f51b Adds communities to the readme 2023-07-06 15:50:52 -04:00
Vitor Pamplona
3e0202e6cb v0.66.0 2023-07-06 11:44:33 -04:00
Vitor Pamplona
d59a68ec2e Formatting 2023-07-06 11:41:11 -04:00
Vitor Pamplona
f249884907 Fixes the participants on a community index 2023-07-06 11:41:03 -04:00
Vitor Pamplona
c823d1689f Displays Approval Notes in the notifications 2023-07-06 11:21:35 -04:00
Vitor Pamplona
b10f268e69 - Adds a button to create community posts from the Community screen
- Adds a reference to the reply/community in the new post screen.
2023-07-06 10:57:41 -04:00
Vitor Pamplona
eff21c4f8f Minimizes the height of the tabs 2023-07-06 10:22:44 -04:00
Vitor Pamplona
5537208abb - Adds Basic Support for Communities (172)
- Adds Community discovery screens
- Adds Public Chat discovery
- Adds Community Follow/Unfollow
- Renders Community Posts in feed
- Creates a summary of verified participants in Communities/Chats/Streams
- Restructures Hashtag Screen to the new Screen Building structure
- Remembers scroll position in Live, Community and Chat discovery
2023-07-06 10:11:25 -04:00
Vitor Pamplona
53a5d3e88e Merge pull request #491 from ShinoharaTa/fix_broken_japanese_view
Fixed a bug in Japanese string display
2023-07-05 15:04:28 -04:00
Vitor Pamplona
20f840fd99 Merge pull request #489 from davotoula/translations_update
Updated se/cs/ge translations
2023-07-05 15:00:47 -04:00
tshinohara
dc940c0608 fix lint error. 2023-07-06 03:05:17 +09:00
tshinohara
f5c77d41f1 Bugfix: Url parse disabled in other texts. 2023-07-06 02:47:29 +09:00
tshinohara
014c613577 Bugfix: Change matching pattern. 2023-07-06 01:26:15 +09:00
tshinohara
d369c2c658 Bugfix: Japanese url export disable 2. 2023-07-06 01:23:31 +09:00
tshinohara
536868d3e5 Bugfix: Japanese url export disable. 2023-07-06 01:20:54 +09:00
Vitor Pamplona
5706738898 Initial Support for Communities 2023-07-05 11:22:03 -04:00
Vitor Pamplona
2947c2c7d0 Replaces the Live bar in the top of the feed for Notification dots in the LiveStream bottom button. 2023-07-05 10:28:21 -04:00
Vitor Pamplona
18ac527629 BugFix for invalid hexes in the hex index 2023-07-05 09:09:17 -04:00
Vitor Pamplona
6d80d14421 Updates dependencies 2023-07-05 09:04:42 -04:00
David Kaspar
e60d8c5c20 Updated se/cs/ge translations 2023-07-05 13:03:27 +01:00
Vitor Pamplona
51b4cf444a v0.65.1 2023-07-04 21:02:22 -04:00
Vitor Pamplona
f78ec5cc90 Performance updates 2023-07-04 20:55:01 -04:00
Vitor Pamplona
fd6a63a76c Fixes https://github.com/vitorpamplona/amethyst/issues/487 2023-07-04 19:25:21 -04:00
Vitor Pamplona
e0f186bb94 BugFix for not rendering images on markdown when there is no NIP19 reference in the text. 2023-07-04 12:19:56 -04:00
Vitor Pamplona
9fadab5bb9 v0.65.0 2023-07-04 11:45:35 -04:00
Vitor Pamplona
ea768fb560 Base Streaming view 2023-07-04 11:44:28 -04:00
Vitor Pamplona
71cb18f360 v0.64.4 2023-07-03 08:53:30 -04:00
Vitor Pamplona
3a1694ac9a Renames Hex Check method for better understanding. 2023-07-03 08:36:33 -04:00
Vitor Pamplona
91c47b6e87 BugFix for delayed update of the last messages in the Messages screen 2023-07-03 08:36:12 -04:00
Vitor Pamplona
fe4e03df39 v0.64.3 2023-07-02 16:38:28 -04:00
Vitor Pamplona
d110b9fbbf BugFix for IndexOutOfBoundsException 2023-07-02 15:50:07 -04:00
Vitor Pamplona
dc8c177f20 New Icon for verified follows 2023-07-02 15:47:07 -04:00
Vitor Pamplona
1beacd240e Fixing the lack of the checkmark in the verification icons 2023-07-02 15:46:41 -04:00
Vitor Pamplona
083394b928 Merge pull request #485 from believethehype/main
Added #Cashu
2023-07-02 14:31:54 -04:00
Believethehype
6de8d9d7e0 Update HashtagIcon.kt 2023-07-02 17:02:58 +02:00
Vitor Pamplona
fb4457e767 Fix for last PR 2023-07-01 18:58:01 -04:00
Vitor Pamplona
d4c1907349 Merge pull request #484 from AutumnSunshine/AutumnSunshine-updated-Tamil-translation
Update strings.xml for Tamil translation (values-ta)
2023-07-01 18:42:29 -04:00
AutumnSunshine
c92926f3ab Update strings.xml
Updated Tamil translations for newer additions, building on Vivek's earlier translation.
2023-07-02 03:22:15 +05:30
Vitor Pamplona
8f4f8f7953 v0.64.2 2023-07-01 17:27:00 -04:00
Vitor Pamplona
5a3ea1c258 Shows channel picture on the public chat channel's header instead of the creator's picture. 2023-07-01 17:24:54 -04:00
Vitor Pamplona
a567ec70ad v0.64.1 2023-07-01 17:19:42 -04:00
Vitor Pamplona
e402081777 Adds a Toast for not having a private key when following/unfollowing people 2023-07-01 17:16:56 -04:00
Vitor Pamplona
ed9c27341f BugFix for not changing background color on Notes 2023-07-01 17:16:32 -04:00
Vitor Pamplona
56d9c9a50f Moves away from drawBehind and uses background color instead. It feels faster at first glance. 2023-07-01 16:57:45 -04:00
Vitor Pamplona
d2a22f4ca0 Moves from drawBehind to .background because it's faster. 2023-07-01 15:52:51 -04:00
Vitor Pamplona
09582fd0b1 Removes the need to draw a background with the theme's background color on the verification symbol 2023-07-01 15:33:13 -04:00
Vitor Pamplona
1d5dfbfd29 Reduces the max grouped reactions on Notifications to 30 to increase speed of scroll. 2023-07-01 11:58:40 -04:00
Vitor Pamplona
044d47adad Merge remote-tracking branch 'origin/HEAD' 2023-07-01 11:52:51 -04:00
Vitor Pamplona
cd9465c0e7 Moving screen updates from 300ms to 500ms to avoid triggering updates while animations are running. 2023-07-01 11:52:40 -04:00
Vitor Pamplona
d179c93c0b New RichText engine to help with testing classes. 2023-07-01 11:51:46 -04:00
Vitor Pamplona
3c84eae647 Merge pull request #481 from greenart7c3/main
fix relay tab not working in profile page
2023-06-30 20:45:58 -04:00
greenart7c3
53a146d0e5 fix relay tab not working in profile page 2023-06-30 20:28:40 -03:00
Vitor Pamplona
57db7f0272 Moves some of the modifiers to singleton variables 2023-06-30 08:58:28 -04:00
Vitor Pamplona
b820b6564f BugFix Crashing relay screen on invalid URLs 2023-06-30 08:58:14 -04:00
Vitor Pamplona
3908c42c7f Reduces the use of hex 2023-06-30 08:57:57 -04:00
Vitor Pamplona
1793fe40bf v0.64.0 2023-06-29 18:13:48 -04:00
Vitor Pamplona
0857695332 Moves global to home as a list and search to the top bar. 2023-06-29 18:13:09 -04:00
Vitor Pamplona
a7b3bac8f8 Slims down the live headers 2023-06-29 18:12:22 -04:00
Vitor Pamplona
e6f122d8b7 BugFix for trimmed paragraphs losing indentation 2023-06-29 16:46:44 -04:00
Vitor Pamplona
b2cdc2ddef BugFix: Not showing display name on [n] tags 2023-06-29 16:28:47 -04:00
Vitor Pamplona
94057059bb Logging out the time to display reactions 2023-06-29 16:17:29 -04:00
Vitor Pamplona
927ef17896 Improved AsyncImage loading states. 2023-06-29 15:09:08 -04:00
Vitor Pamplona
33f915879b Makes NIP05 work in domain names only (No need for the _@ in the "_@domain" address) 2023-06-29 15:08:46 -04:00
Vitor Pamplona
98bca48359 Changes to not preview images when CanPreview is false. 2023-06-29 14:19:35 -04:00
Vitor Pamplona
171f763908 Additional checks for main thread in Notifications 2023-06-29 12:42:11 -04:00
Vitor Pamplona
605d83e530 v0.63.0 2023-06-29 12:32:45 -04:00
Vitor Pamplona
5f710b7304 Adds #tunestr icon 2023-06-29 12:29:08 -04:00
Vitor Pamplona
1c6593fb17 Adds alt-text to screenshots 2023-06-29 12:15:03 -04:00
Vitor Pamplona
33fe34d3b3 updates screenshot titles 2023-06-29 12:13:56 -04:00
Vitor Pamplona
e257fec92c Breaking screenshots in two lines 2023-06-29 12:10:26 -04:00
Vitor Pamplona
6a70a6c72f Showing screenshots in the homepage 2023-06-29 12:09:05 -04:00
Vitor Pamplona
270d6b411a Updating Amethyst Logo to Zitron's 2023-06-29 12:00:20 -04:00
Vitor Pamplona
803b1e861e Merge pull request #480 from greenart7c3/main
added followed tags in profile screen
2023-06-29 10:03:33 -04:00
Vitor Pamplona
7300dc3d78 No need to point to the release section after the GitHub badge has been added. 2023-06-29 09:52:24 -04:00
Vitor Pamplona
405d64402b decreases height 2023-06-29 09:42:55 -04:00
Vitor Pamplona
fe1bff1074 removes space between badges 2023-06-29 09:41:44 -04:00
Vitor Pamplona
2952ca1978 Reverting change 2023-06-29 09:39:14 -04:00
Vitor Pamplona
32dcd7747b slight change in borders 2023-06-29 09:38:20 -04:00
Vitor Pamplona
eca5a1de15 Matching obtainium's and github's badge 2023-06-29 09:36:55 -04:00
Vitor Pamplona
886442ac52 trying to align obtainium's image on the readme 2023-06-29 09:35:20 -04:00
Vitor Pamplona
8521611008 Increasing borders a bit 2023-06-29 09:32:38 -04:00
Vitor Pamplona
df647e4047 Fixing borders 2023-06-29 09:31:44 -04:00
Vitor Pamplona
af26026d25 adds borders 2023-06-29 09:29:55 -04:00
Vitor Pamplona
43be55f3d6 reduces the size of the badge 2023-06-29 09:26:32 -04:00
Vitor Pamplona
9b3cb92cb3 Adds obtainium and APK to the links 2023-06-29 09:25:35 -04:00
Vitor Pamplona
f2b0ce5fd0 adds obtainium image 2023-06-29 09:24:42 -04:00
Vitor Pamplona
787ca98daf v0.62.8 2023-06-28 19:06:05 -04:00
Vitor Pamplona
71517dc454 Formatting 2023-06-28 18:54:42 -04:00
Vitor Pamplona
949301d58f Mocking Looper to use the checkMainThread in tests 2023-06-28 18:54:33 -04:00
Vitor Pamplona
cf8ac4faae Merge remote-tracking branch 'origin/HEAD'
# Conflicts:
#	app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
2023-06-28 18:32:59 -04:00
Vitor Pamplona
ad3aa124fc Performance improvements additional MainThread checks. 2023-06-28 18:32:10 -04:00
Vitor Pamplona
2326aa9f7c Runs the translation update in the main thread 2023-06-28 14:52:01 -04:00
Vitor Pamplona
acfd687d74 Avoids blinking the nip05 address 2023-06-28 14:51:34 -04:00
Vitor Pamplona
9b9183395d BugFix: Ignoring case when comparing original and translated versions. 2023-06-28 14:51:02 -04:00
greenart7c3
45b4902b2b Merge branch 'vitorpamplona:main' into main 2023-06-28 15:40:36 -03:00
greenart7c3
6056d60d32 added followed tags in profile screen 2023-06-28 15:37:24 -03:00
Vitor Pamplona
61af248c09 Merge pull request #479 from ZsZolee/main
Additional Hungarian translation
2023-06-28 11:17:16 -04:00
Vitor Pamplona
5a580db1b8 Merge pull request #478 from greenart7c3/main
fix rendering url of highlight note
2023-06-28 11:17:03 -04:00
Vitor Pamplona
a72ae49e93 Merge pull request #477 from believethehype/cashuredeem
Add copy button to Cashu Rendering, better default error message
2023-06-28 11:16:08 -04:00
Vitor Pamplona
40a01219cc Moves RichText Modifiers to singleton classes. 2023-06-28 11:13:38 -04:00
Vitor Pamplona
c652390a9c Updates URLPreview in the main thread. 2023-06-28 11:09:54 -04:00
Vitor Pamplona
0916a97526 Users rememberPagerState instead of just remember 2023-06-28 11:09:19 -04:00
Vitor Pamplona
d76ef0a1ca updates firebase to the newest version 2023-06-28 11:06:56 -04:00
Zoltan
3a70ce1587 Additional Hungarian translation 2023-06-28 16:52:39 +02:00
greenart7c3
9f96e84378 fix rendering url of highlight note 2023-06-28 07:58:19 -03:00
Believethehype
985771beba Add copy button to Cashu Rendering, better default error message 2023-06-28 11:49:14 +02:00
Vitor Pamplona
3967633f77 Merge pull request #475 from davotoula/translation_updates
Updated se/de/cs translations
2023-06-27 15:05:31 -04:00
Vitor Pamplona
ee5699c2de v0.62.7 2023-06-27 13:16:28 -04:00
Vitor Pamplona
b8f4d9f0bb BugFix on zapraiser completed bar 2023-06-27 13:14:47 -04:00
Vitor Pamplona
8b1de47517 v0.62.6 2023-06-27 12:27:06 -04:00
Vitor Pamplona
c6ff514ba2 Logs rendering time of sections of the post 2023-06-27 12:26:57 -04:00
Vitor Pamplona
50f2d18b61 Moves state assignments to the main thread. 2023-06-27 12:05:11 -04:00
Vitor Pamplona
fa7ccdfc20 Reduces full screen updates to once a second. 2023-06-27 12:02:55 -04:00
David Kaspar
fe02519737 Updated se/de/cs translations 2023-06-27 13:30:37 +01:00
Vitor Pamplona
f2e9fbebea v0.62.5 2023-06-26 22:42:42 -04:00
Vitor Pamplona
52af109b4e Minor adjustments in threading 2023-06-26 20:38:06 -04:00
Vitor Pamplona
833e6bc3e1 Moves cashu processing to a state class to account for errors. 2023-06-26 20:17:35 -04:00
Vitor Pamplona
9bed6fdb9a v0.62.4 2023-06-26 18:22:07 -04:00
Vitor Pamplona
a4a867ebcd Achieves a bigger space if the hide system bars is placed outside the Dialog. 2023-06-26 18:18:04 -04:00
Vitor Pamplona
4da2f18957 Video Dialog in full screen 2023-06-26 17:44:36 -04:00
Vitor Pamplona
4141a52d51 Allows video dialog on streaming 2023-06-26 15:40:13 -04:00
Vitor Pamplona
4dbc71d577 Removes padding from the live header 2023-06-26 15:39:32 -04:00
Vitor Pamplona
3b5d792562 Extracts Cashu logic into a service class. 2023-06-26 15:27:04 -04:00
Vitor Pamplona
dd7f3225c6 Preloading channel from LocalCache 2023-06-26 14:59:18 -04:00
Vitor Pamplona
3477e7b8ba No need to remember Modifier 2023-06-26 14:55:55 -04:00
Vitor Pamplona
a3782bb459 Remove image from the live event and use only the author picture in the header. 2023-06-26 14:47:22 -04:00
Vitor Pamplona
d4003eee04 Adjusts Profile images to crop and not fit 2023-06-26 14:46:56 -04:00
Vitor Pamplona
c9bbb66284 Adjusting logout message. 2023-06-26 14:33:21 -04:00
Vitor Pamplona
1498fa9868 Merge pull request #474 from greenart7c3/main
add confirmation dialog before logout
2023-06-26 14:27:15 -04:00
Vitor Pamplona
26b493aa1d Merge pull request #473 from believethehype/cashuredeem
Melt cashu tokens in Mint directly instead of using webservice
2023-06-26 14:26:44 -04:00
Vitor Pamplona
f9d652f849 Performance Improvements 2023-06-26 14:25:46 -04:00
greenart7c3
30b0ee444f add confirmation dialog before logout 2023-06-26 14:38:44 -03:00
Believethehype
bf8341ef58 Merge remote-tracking branch 'upstream/main' into cashuredeem 2023-06-26 18:19:01 +02:00
Believethehype
f9f1b0d98e Melt cashu tokens in Mint directly instead of using webservice 2023-06-26 18:18:45 +02:00
Vitor Pamplona
743a4c9d87 Using crossfade to show live activities 2023-06-26 09:12:10 -04:00
Vitor Pamplona
275baa7497 Merge pull request #472 from believethehype/cashuredeem
Parse and Show Sat Amount from Cashu tokens
2023-06-26 08:44:19 -04:00
Vitor Pamplona
ac1fb879c3 Bugfix for not updating user picture and name after a message arrives 2023-06-26 08:39:46 -04:00
Vitor Pamplona
270ad56450 Aligns LIVE marker to the right in the feed. 2023-06-26 08:38:29 -04:00
Believethehype
d155241efc Parse and Show Sat Amount from Cashu tokens 2023-06-26 14:32:08 +02:00
Vitor Pamplona
d61927cbee Merge pull request #471 from believethehype/cashuredeem
Parse Cashu Tokens and provide link to redeem
2023-06-26 06:50:56 -04:00
Vitor Pamplona
996b68f9c6 Merge pull request #470 from Bardesss/patch-1
Update strings.xml (NL)
2023-06-26 06:49:19 -04:00
Believethehype
f6913663d1 enable autopay, change parsing 2023-06-26 11:57:31 +02:00
Believethehype
58600e13a8 parse cashu tokens 2023-06-26 11:04:31 +02:00
Bardesss
54e15ad3cd Update strings.xml (NL)
Updated translation
2023-06-26 08:52:35 +00:00
Vitor Pamplona
777c652bf5 v0.62.3 (TEST) 2023-06-25 19:35:00 -04:00
Vitor Pamplona
7fb1473e21 There is no need to invalidate twice. 2023-06-25 19:34:51 -04:00
Vitor Pamplona
95db1ec84f Typo 2023-06-25 19:34:35 -04:00
Vitor Pamplona
7438973a20 Trying to improve rendering speed of chat messages. 2023-06-25 19:18:32 -04:00
Vitor Pamplona
fe31855860 Minor performance improvements in reaction row 2023-06-25 19:18:11 -04:00
Vitor Pamplona
d7bce4414d Crossfades changes in Reactions and Zaps 2023-06-25 19:17:48 -04:00
Vitor Pamplona
b4a81c137c Removes the need for lifecycle awareness here. 2023-06-25 19:16:41 -04:00
Vitor Pamplona
a6390f58d9 Removes debug info 2023-06-25 19:16:15 -04:00
Vitor Pamplona
ca3f4c3ec6 Moves MaxWidth for Chat bubbles to Shapes 2023-06-25 16:49:33 -04:00
Vitor Pamplona
53bfd40a31 Simplifies hex check 2023-06-25 16:22:17 -04:00
Vitor Pamplona
798be25c43 BugFix for duplicated notes in the LocalCache 2023-06-25 16:21:36 -04:00
Vitor Pamplona
ebe1bcedd0 Moves user msg Synchronization to it's own object. 2023-06-25 16:11:21 -04:00
Vitor Pamplona
4731575b87 Less memory intensive Hex Checker 2023-06-25 12:41:21 -04:00
Vitor Pamplona
383e761372 Moves LocalCache's Synchronized block to Concurrent implementations 2023-06-25 12:40:56 -04:00
Vitor Pamplona
54a95e6a4e Moves synchronized block to inner function 2023-06-25 12:38:08 -04:00
Vitor Pamplona
3105ade755 Moves Shape constants to the Shape class 2023-06-25 11:00:41 -04:00
Vitor Pamplona
f651034303 Better starting point for the following observer 2023-06-24 18:08:11 -04:00
Vitor Pamplona
7554dc010c Fixes spacing from last change 2023-06-24 18:07:56 -04:00
Vitor Pamplona
70a962062b Minor code and performance improvements. 2023-06-24 18:03:04 -04:00
Vitor Pamplona
5cc7f3bf2b Correct Immutable class 2023-06-24 15:55:21 -04:00
Vitor Pamplona
b040e0ca13 Moves OnlineChecker to a singleton class with cache. 2023-06-24 15:53:22 -04:00
Vitor Pamplona
775094356c Fixes new posts not showing up in the feed. 2023-06-24 10:49:34 -04:00
Vitor Pamplona
0771b179b5 Reducing the need to update the event for every two hours. 2023-06-24 10:36:42 -04:00
Vitor Pamplona
8af307e8bb Migrating the UserProfile page to the new ViewModel structure 2023-06-23 22:00:51 -04:00
Vitor Pamplona
e6ec3f1ce4 - Adds picture of the User to the Stream header.
- BugFix for the Click on UserPicture event
2023-06-23 20:44:58 -04:00
Vitor Pamplona
b4395e7585 v0.62.2 2023-06-23 18:10:25 -04:00
Vitor Pamplona
f8acb26763 Only showing the paid relay icon when the relay is paid. 2023-06-23 18:09:03 -04:00
Vitor Pamplona
aa931a4cc5 Improves Logs when the app cannot connect with relays. 2023-06-23 18:02:04 -04:00
Vitor Pamplona
e1cf188fbe Displays an error message when it cannot parse the returning json from relays. 2023-06-23 17:57:21 -04:00
Vitor Pamplona
9e57a706ae Moves some of the account observables to maps 2023-06-23 17:57:02 -04:00
Vitor Pamplona
8c73e981b5 formatting 2023-06-23 17:56:25 -04:00
Vitor Pamplona
87f5007fd3 Displays error message on LogCat if fails to login 2023-06-23 17:56:13 -04:00
Vitor Pamplona
5c42bb0282 Removes check in the Main Thread to allow login on Debug 2023-06-23 17:55:51 -04:00
Vitor Pamplona
415252b770 Converts TopBar Observables to Maps. 2023-06-23 17:55:30 -04:00
Vitor Pamplona
4f43d7cde5 More updates to the readme 2023-06-23 15:22:09 -04:00
Vitor Pamplona
c819a23f83 updating Readme 2023-06-23 15:16:57 -04:00
Vitor Pamplona
4a00a9a80d v0.62.1 2023-06-23 15:11:08 -04:00
Vitor Pamplona
1f5f6b251c BugFix on publishedAt date for Longform posts 2023-06-23 15:08:26 -04:00
Vitor Pamplona
c925b82e86 Stops showing posts in the future 2023-06-23 14:30:26 -04:00
Vitor Pamplona
96a5d1a658 v0.62.0 2023-06-23 14:11:56 -04:00
Vitor Pamplona
36154422f1 Formatting 2023-06-23 13:41:04 -04:00
Vitor Pamplona
5b2a878f5d Moving the NIP98 event to it's own class. 2023-06-23 13:40:57 -04:00
Vitor Pamplona
976d96ef35 formatting 2023-06-23 12:27:08 -04:00
Vitor Pamplona
94b31fe5f9 Merge pull request #466 from believethehype/main
added NIP98 Support, Added nostrcheck.me Image hoster
2023-06-23 12:26:51 -04:00
Vitor Pamplona
cd2a59ae43 Merge pull request #465 from greenart7c3/main
show icon for paid relays in the relay list
2023-06-23 12:18:23 -04:00
Vitor Pamplona
e8f5ef3aa3 Removes check in the main thread to avoid mocking Android's threading system 2023-06-23 12:10:46 -04:00
Vitor Pamplona
887fc33073 Minor performance adjustments across the app. 2023-06-23 12:02:27 -04:00
Vitor Pamplona
c24ba18207 Avoids calling out errors in LogCat when they don't exist. 2023-06-23 12:01:46 -04:00
Vitor Pamplona
5137009f5f Improves rendering time of the Channel header 2023-06-23 11:36:50 -04:00
Vitor Pamplona
ba6bd6a2d2 Making more classes stable for performance reasons. 2023-06-23 11:36:08 -04:00
Believethehype
bfc16d0878 added some wait time before updating status 2023-06-23 16:39:40 +02:00
Believethehype
94c41e0c6d added NIP98 Support, Added nostrcheck.me hoster 2023-06-23 16:10:04 +02:00
Vitor Pamplona
33454cc2f8 Fixes UTF-32 Reactions 2023-06-23 09:21:34 -04:00
Believethehype
d969a483a9 Merge remote-tracking branch 'upstream/main' 2023-06-23 12:50:22 +02:00
greenart7c3
a51104fd82 show icon for paid relays in the relay list 2023-06-23 06:51:21 -03:00
Vitor Pamplona
a536388123 Avoids crashing due to illegal zap address 2023-06-22 19:39:55 -04:00
Vitor Pamplona
c7cb4b7015 Adds logs to Init functions of classes to keep a close eye on. 2023-06-22 17:41:55 -04:00
Vitor Pamplona
fbb1b59e34 Adds checks for the main thread when parsing NIP30 2023-06-22 17:41:11 -04:00
Vitor Pamplona
7d7f047f55 removes unused variable 2023-06-22 15:23:16 -04:00
Vitor Pamplona
e9e0b0f57f Adds more checks for not in the main thread 2023-06-22 15:23:04 -04:00
Vitor Pamplona
baf0372537 Moves the image border modifier to an app variable. 2023-06-22 15:16:17 -04:00
Vitor Pamplona
f62716a226 formatting 2023-06-22 15:11:55 -04:00
Vitor Pamplona
9b1c47f4a5 Checks main thread in preferences 2023-06-22 14:53:10 -04:00
Vitor Pamplona
a4dd3aa488 Moves checks to run to outside the LaunchedEffect 2023-06-22 14:30:22 -04:00
Vitor Pamplona
9dbe7379cd Fixes reaction row space. 2023-06-22 14:25:18 -04:00
Vitor Pamplona
ad353d2d1a Generalizes the SensitivityWarning to Note. 2023-06-22 12:04:11 -04:00
Vitor Pamplona
32b2b1ace8 Uses derivedOf to protect from updates with the same content 2023-06-22 12:03:52 -04:00
Vitor Pamplona
13710dde06 No need to go into a Synchronized block to get a user object here. 2023-06-22 11:49:49 -04:00
Vitor Pamplona
915ddd0bad Improving relay options rendering performance. 2023-06-22 11:49:27 -04:00
Vitor Pamplona
52ef356c06 Minor performance improvements throughout the code 2023-06-22 11:39:56 -04:00
Vitor Pamplona
69481a046b v0.61.4 2023-06-21 14:56:44 -04:00
Vitor Pamplona
157802142c Reorganizing Relay Info Page 2023-06-21 14:51:44 -04:00
Vitor Pamplona
54462f8d26 Merge pull request #462 from greenart7c3/relay_information
Relay information dialog
2023-06-21 14:03:34 -04:00
Vitor Pamplona
fcf612d22a Merge pull request #461 from ZsZolee/main
Zapraiser Hungarian translation
2023-06-21 14:02:46 -04:00
Vitor Pamplona
e8eea4be25 Removes old image proxy classes 2023-06-21 12:47:53 -04:00
Vitor Pamplona
cce9d6cf68 Improves rendering performance of Chat Screens 2023-06-21 11:57:49 -04:00
greenart7c3
143f58fd68 add string resources 2023-06-21 11:38:34 -03:00
Vitor Pamplona
6fe66986be Improves rendering of chat messages when scrolling. 2023-06-21 10:23:49 -04:00
greenart7c3
68e8abecb2 fix lints 2023-06-21 11:23:06 -03:00
greenart7c3
bb698a351e fix build after rebasing 2023-06-21 10:47:27 -03:00
greenart7c3
4506ff9c93 created dialog to show relay information 2023-06-21 10:43:34 -03:00
Zoltan
f88a4d56c7 Zapraiser Hungarian translation 2023-06-21 10:12:48 +02:00
Vitor Pamplona
fbe0f584af v0.61.3 2023-06-20 19:00:46 -04:00
Vitor Pamplona
e28f26d471 BugFix for opening the video chat with video in full screen until it loads. 2023-06-20 18:59:21 -04:00
Vitor Pamplona
80a9a2a712 Fix margins when summary is not available. 2023-06-20 18:58:56 -04:00
Vitor Pamplona
2a47cd4fd1 BugFix for too many streams (fix in max = 2 for now) 2023-06-20 18:58:41 -04:00
Vitor Pamplona
ce207aed39 Refreshing home when follows change, including Live activities. 2023-06-20 18:27:21 -04:00
Vitor Pamplona
244e3f3b29 v0.61.2 2023-06-20 17:13:24 -04:00
Vitor Pamplona
6a44d283f9 - Restructures Chat Message Rendering
- Adds Sensitive Content protections for chat renderings.
- Adjusts padding of the channel headers.
2023-06-20 16:53:36 -04:00
Vitor Pamplona
6addce3d20 Simplifying some of the chat rendering process 2023-06-20 15:16:29 -04:00
Vitor Pamplona
8aae101ef6 Adds live activities to the main feed. 2023-06-20 13:54:47 -04:00
Vitor Pamplona
d1bbdef5c4 v0.61.1 2023-06-19 22:01:30 -04:00
Vitor Pamplona
b78967f8be Screen diet 2023-06-19 21:58:12 -04:00
Vitor Pamplona
01a2f746d0 Adds space between the play button and the user's name in private messages 2023-06-19 21:10:01 -04:00
Vitor Pamplona
d063c2990c Moves the label to the bottom of the video 2023-06-19 20:58:38 -04:00
Vitor Pamplona
e31ee031cb Removes the cache from streaming files to make sure the stream keeps playing. 2023-06-19 20:53:38 -04:00
Vitor Pamplona
8fc489cc59 Formatting 2023-06-19 20:43:55 -04:00
Vitor Pamplona
ffb2a19963 Adds Zaps and Reactions to the Zap Activity author 2023-06-19 20:43:48 -04:00
Vitor Pamplona
b271c1775c BugFix for not updating Notifications quickly. 2023-06-19 20:30:13 -04:00
Vitor Pamplona
f937e95b35 v0.61.0 2023-06-19 18:36:32 -04:00
Vitor Pamplona
1613e79860 Support for live chats in streaming events. 2023-06-19 18:34:52 -04:00
Vitor Pamplona
dd40b4f205 Formatting 2023-06-19 16:24:58 -04:00
Vitor Pamplona
2ad9401ee6 Downloads last posts from the author. 2023-06-19 16:23:19 -04:00
Vitor Pamplona
69ade2f6e6 Merge remote-tracking branch 'origin/HEAD' 2023-06-19 16:02:23 -04:00
Vitor Pamplona
4006e62a82 Renders Channel headers, NIP94 and NIP95 in the thread head. 2023-06-19 16:01:51 -04:00
Vitor Pamplona
ec88a8b157 Updates readme 2023-06-19 16:00:23 -04:00
Vitor Pamplona
128211ecd2 Merge pull request #459 from greenart7c3/main
add toast message to show relay icon description
2023-06-19 15:32:46 -04:00
Vitor Pamplona
8f53537ebc v0.60.2 2023-06-19 14:43:34 -04:00
Vitor Pamplona
58c93e0cce - Redesign feed invalidations to account for changes in the follow list
- Redesign scroll to the top implementation to avoid using arguments in the navigator.
2023-06-19 14:30:15 -04:00
greenart7c3
c35b5e3703 add toast message for relay icons 2023-06-19 07:58:25 -03:00
Vitor Pamplona
be8848fb1b Restructures Stable elements for minor performance gains. 2023-06-18 18:57:21 -04:00
Vitor Pamplona
a3efd15b95 v0.60.1 2023-06-18 14:43:59 -04:00
Vitor Pamplona
a1930c232b Formatting 2023-06-18 14:42:26 -04:00
Vitor Pamplona
58a4fb18ae BugFixes for NPE 2023-06-18 14:40:46 -04:00
Vitor Pamplona
be4870da1a Support for Per-post Zap-raisers. 2023-06-18 14:24:52 -04:00
Vitor Pamplona
d2f6317d5c v0.60.0 2023-06-17 21:09:11 -04:00
Vitor Pamplona
05fb408842 - Adds Support for Kind 16
- Migrates citations to `e` when they were supposed to be `a`
- Removes original tags from Reposts
-
2023-06-17 21:08:33 -04:00
Vitor Pamplona
611305a406 Basic Support for live activities 2023-06-17 18:37:34 -04:00
Vitor Pamplona
5b856af19b - Activates support for m3u8 streaming
- BugFix on restarting the video when pressing mute.
2023-06-17 18:07:06 -04:00
Vitor Pamplona
ce5106684f Moves the last seen time saved per route to an account property in order to speed up loading and facilitate multithreading in the app. 2023-06-17 17:28:14 -04:00
Vitor Pamplona
f0fe2dfc17 Don't move the feed up when new follow list is updated 2023-06-17 15:56:46 -04:00
Vitor Pamplona
fb5fad5d02 Fixing background color for messages when it's considered a new notification. 2023-06-17 10:53:37 -04:00
Vitor Pamplona
6314e3fcb2 Formatting 2023-06-16 19:59:12 -04:00
Vitor Pamplona
da62760fd7 Moving static sizes to the Shape class 2023-06-16 19:57:25 -04:00
Vitor Pamplona
e3eae80d4c Increasing performance of the follow/follower count 2023-06-16 19:11:14 -04:00
Vitor Pamplona
3cd0828cba Bugfix for home feed scrolling to the top when a new follower updates the cache. 2023-06-16 18:47:10 -04:00
Vitor Pamplona
806b86f950 Testing a release build script with deamon 2023-06-16 16:36:20 -04:00
Vitor Pamplona
c931e2cbee v0.59.1 2023-06-16 16:21:23 -04:00
Vitor Pamplona
4e57eed5d8 Adds Name Playback on messages as well. 2023-06-16 16:18:06 -04:00
Vitor Pamplona
8e725259eb BugFix for Messages being marked as read from Notifications. 2023-06-16 16:17:42 -04:00
Vitor Pamplona
b48d2df8d2 v0.59.0 2023-06-16 15:23:44 -04:00
Vitor Pamplona
dbf34ae457 Text to speech for usernames 2023-06-16 15:22:44 -04:00
Vitor Pamplona
74be724835 Hide reposts of posts from people you already follow if the repost time is within the hour of the original post (not that far in the feed). 2023-06-16 14:31:52 -04:00
Vitor Pamplona
012c7a7105 BugFix for feed not updating after change in follows. 2023-06-16 14:30:22 -04:00
Vitor Pamplona
e50dff3b62 BigFix for some reposts not showing up in the feed. 2023-06-16 14:15:53 -04:00
Vitor Pamplona
ad5a4d0b59 v0.58.3 2023-06-15 18:51:39 -04:00
Vitor Pamplona
3116ea1ad9 Fixes reaction sizes and alignments. 2023-06-15 12:21:44 -04:00
Vitor Pamplona
72e474c9ec Bug fix for when deleting the reaction event reverting to the old like heart. 2023-06-15 09:37:09 -04:00
Vitor Pamplona
fe9e85c282 v0.58.2 2023-06-14 20:58:51 -04:00
Vitor Pamplona
01ac90fbec BugFix for active like all the time 2023-06-14 20:55:46 -04:00
Vitor Pamplona
dc079941e7 v0.58.1 2023-06-14 20:28:47 -04:00
Vitor Pamplona
9a7a94cb77 Cropping long reactions to two chars for emojis 2023-06-14 20:27:46 -04:00
Vitor Pamplona
a44703cb49 v0.58.0 2023-06-14 16:50:54 -04:00
Vitor Pamplona
52bd6b4443 Custom reactions 2023-06-14 16:49:51 -04:00
Believethehype
279894f9d7 Merge remote-tracking branch 'upstream/main' 2023-06-14 22:31:07 +02:00
Vitor Pamplona
a9e1ce5aec Removing modifiers from remembers and into global singletons. 2023-06-14 14:36:10 -04:00
Vitor Pamplona
639fdb174d BugFix Add -> And 2023-06-14 14:32:55 -04:00
Vitor Pamplona
3cd02ee6cf Merge remote-tracking branch 'origin/HEAD' 2023-06-14 13:49:10 -04:00
Vitor Pamplona
52199fe281 Starts to use getXIfExists functions to preload objects 2023-06-14 13:48:58 -04:00
Vitor Pamplona
5616fa52d1 Simplifies URL Preview call 2023-06-14 13:48:35 -04:00
Vitor Pamplona
7241c8bd9f Merge pull request #454 from ZsZolee/main
Small Hungarian translation
2023-06-14 10:55:16 -04:00
Zoltan
991131f8cd Small Hungarian translation 2023-06-14 09:35:36 +02:00
Vitor Pamplona
a24c95070c v0.57.0 2023-06-13 17:16:20 -04:00
Vitor Pamplona
a346d31554 Finally adds markers to kind 1s 2023-06-13 17:08:50 -04:00
Vitor Pamplona
9b5c7ad23f - Moves zaps from a Map<Request,Response> to a List<CustomClass> to avoid recompositions due to the use of the unstable Pair object
- Creates the class ZapAmountCommentNotification to be used instead of the unstable Triple class.
2023-06-13 16:33:52 -04:00
Vitor Pamplona
a01a060abc Uses animateItemPlacement to show new Notes and update the size of current ones. 2023-06-13 16:31:23 -04:00
Vitor Pamplona
1cd39e0d8f Removes the use of by on the quick action menu's state because it was triggering recompositions 2023-06-13 16:30:37 -04:00
Vitor Pamplona
305ecc6a2e New update removes access to the backQueue 2023-06-13 16:29:21 -04:00
Vitor Pamplona
858f94ae74 Moves icon sizes in the bottom row to remember functions 2023-06-13 16:28:42 -04:00
Vitor Pamplona
fc224d99e3 Updates dependencies. 2023-06-13 10:52:29 -04:00
Vitor Pamplona
b8584964d1 Activates build cache 2023-06-13 10:47:08 -04:00
Vitor Pamplona
fbd0c96a8a Merge remote-tracking branch 'origin/HEAD' 2023-06-12 16:58:32 -04:00
Vitor Pamplona
e2251f9c86 Removing the minimum size for rows in the feed due to Hidden posts from the user's block list. 2023-06-12 16:58:20 -04:00
Vitor Pamplona
eaaacbf218 Turns NIP-05 verification symbol into a State variable to help correctly rendering updates. 2023-06-12 16:56:24 -04:00
Vitor Pamplona
243fbeed44 Remembers some variables on NoteCompose 2023-06-12 16:55:51 -04:00
Vitor Pamplona
d62bab1b08 Makes relay badge list in chatroom Immutable 2023-06-12 16:55:27 -04:00
Vitor Pamplona
70e068ed17 Breaks down user renderer compositions 2023-06-12 16:55:08 -04:00
Vitor Pamplona
824e8795d5 Formatting 2023-06-12 16:54:45 -04:00
Vitor Pamplona
f768a09bd8 Removes unused variable from Translation results 2023-06-12 16:54:15 -04:00
Vitor Pamplona
1a69a5a4c7 Merge pull request #449 from greenart7c3/main
add option to opt-out from automatic spam and report filters
2023-06-12 16:37:03 -04:00
Vitor Pamplona
b9cc3781bb Merge pull request #451 from licaon-kter/patch-1
Typo fix
2023-06-12 11:10:22 -04:00
Licaon_Kter
97964ed65c Typo fix 2023-06-12 15:04:25 +00:00
Vitor Pamplona
42a1d157bf Fixes draft note in privacy.md 2023-06-12 10:25:03 -04:00
Vitor Pamplona
e27efbc9c2 Privacy Policy:
- Removes restrictions of use from the F-Droid version given that F-Droid is taking that legal liability.
- Adds information about the Image Servers offered through the app.
- Adds privacy warnings from the readme to this document.
2023-06-12 10:23:29 -04:00
greenart7c3
57a5d0b618 Merge branch 'main' into main 2023-06-12 07:39:30 -03:00
greenart7c3
8f668cb5fb separated spam and report filter options 2023-06-12 07:38:32 -03:00
Vitor Pamplona
df5ea6f741 Adjusts the size and alignment of relay icons in the video feed 2023-06-11 10:59:18 -04:00
Vitor Pamplona
2e6847df1c v0.56.5 2023-06-10 21:20:57 -04:00
Vitor Pamplona
76a49d6556 Fixing upstream issue with images on markdown. 2023-06-10 21:18:30 -04:00
Vitor Pamplona
59fe3ccf3a Updates readme 2023-06-10 21:18:08 -04:00
Vitor Pamplona
ba884bd1aa Improving markdown support 2023-06-10 21:17:50 -04:00
Vitor Pamplona
e98bb76ee0 Support for NIP-14 2023-06-10 21:16:55 -04:00
Vitor Pamplona
e08bf3eb97 If compression fails, tries to upload with the original file 2023-06-10 21:15:19 -04:00
Vitor Pamplona
9ef02d63a0 Refactors color objects to avoid recreating them on-demand. 2023-06-10 10:08:06 -04:00
Vitor Pamplona
7007796497 v0.56.4 2023-06-09 18:16:42 -04:00
Vitor Pamplona
9f030ffbf8 BugFix for crashing on start up due to creating two caches for videos. 2023-06-09 18:16:03 -04:00
Vitor Pamplona
9ca172e0fc v0.56.3 2023-06-09 17:12:29 -04:00
Vitor Pamplona
360758a2f3 Adds a networking state monitoring services to reconnect when the phone disconnects from relays. 2023-06-09 17:08:35 -04:00
Vitor Pamplona
ffe6f4b55c Fixes F-droid version for mutable colors as background. 2023-06-09 16:43:47 -04:00
Vitor Pamplona
267b5e474c Adds another check to make sure the event does not exist in the local memory before notifying. 2023-06-09 16:41:14 -04:00
Vitor Pamplona
bf6bc5ba68 Moves the recomposition due to background color change to each compose node. 2023-06-09 16:35:56 -04:00
Vitor Pamplona
1efc4c91e2 Reactors base note rendering to wrap the QuickAction menu 2023-06-09 14:07:38 -04:00
Vitor Pamplona
a898196571 Refactoring shapes to the appropriate file. 2023-06-09 13:45:46 -04:00
Vitor Pamplona
c158c7fc3d Moves the clipboard starters to their own compositions. 2023-06-09 13:19:14 -04:00
Vitor Pamplona
e3d83fe39a improving the speed of the highlighter rendering 2023-06-09 11:27:27 -04:00
Vitor Pamplona
5327e1ddb2 Final touches on borders 2023-06-09 11:27:02 -04:00
Vitor Pamplona
a2ad13c54a More spacing refinements for notes. 2023-06-09 10:49:10 -04:00
Vitor Pamplona
816a22ec69 More adjustments to borders and spacing between screen elements for different event types 2023-06-09 10:25:06 -04:00
Vitor Pamplona
8a99dc2db4 Removes unnecessary imports. 2023-06-09 10:03:11 -04:00
Vitor Pamplona
f9cfe1479b - Breaks down the Note Composition stack further
- Fixes some border issues between multiple note types
- Aligns Quick Actions to the center of the note.
2023-06-09 10:02:56 -04:00
Vitor Pamplona
5d8aa7bb58 Refactoring variable name in Reactions 2023-06-09 10:00:37 -04:00
greenart7c3
f0638b1766 add option to opt-out from automatic spam and report filters 2023-06-09 08:28:05 -03:00
Vitor Pamplona
b3e2b78415 v0.56.2 2023-06-08 18:16:53 -04:00
Vitor Pamplona
d0094cc781 Removing duplicated ID protections from Notifications. Looks like we removed all the issues here. 2023-06-08 17:47:10 -04:00
Vitor Pamplona
cb77298f52 Correctly processing multiple matching via find and not match 2023-06-08 17:46:47 -04:00
Vitor Pamplona
24532c8f20 Looks like there is never nothing to restore, so removing this. 2023-06-08 16:31:22 -04:00
Vitor Pamplona
b641684377 Uses default thread to wait for a 250 milliseconds before navigating away from the video upload page. 2023-06-08 16:31:04 -04:00
Vitor Pamplona
9589f68028 Moves sending message on channel from the main thread to the IO thread 2023-06-08 16:30:29 -04:00
Vitor Pamplona
8b852d32e4 BugFix for notifications sometimes not reassembling when pressing the bottom button. 2023-06-08 16:28:48 -04:00
Vitor Pamplona
5b864fa1d9 Adds a second rule for short messages in the spam filter to avoid recognizing "@Bot, command" messages spam. 2023-06-08 15:52:35 -04:00
Vitor Pamplona
8e91987c31 Refactors search screen to make the SearchTextField @Stable 2023-06-08 15:51:46 -04:00
Vitor Pamplona
1c3b34a9c7 Moves the creation of the VideoPlayer to its own composition 2023-06-08 14:22:22 -04:00
Vitor Pamplona
5a44f8d663 Merge pull request #448 from TonyGiorgio/onyx-hashtag
Add onyx hashtag
2023-06-08 12:30:36 -04:00
Vitor Pamplona
2ab7e6b24d v0.56.1 2023-06-08 12:18:06 -04:00
Vitor Pamplona
e9f8fd6cec Merge pull request #447 from TonyGiorgio/weed-hashtag
Add weed hashtag
2023-06-08 12:17:31 -04:00
Tony Giorgio
8d7060afe6 Add onyx hashtag 2023-06-08 11:15:47 -05:00
Vitor Pamplona
784f983746 Image and Video Compression 2023-06-08 12:14:26 -04:00
Tony Giorgio
ecc4141f50 Add weed hashtag 2023-06-08 11:12:20 -05:00
Vitor Pamplona
b964c6a7fa Merge pull request #444 from ZsZolee/main
Adding one small Hungarian translation
2023-06-07 19:17:03 -04:00
Vitor Pamplona
5d37e337bc Merge branch 'main' into main 2023-06-07 19:16:56 -04:00
Vitor Pamplona
d9815b669b v0.56.0 2023-06-07 18:55:34 -04:00
Vitor Pamplona
6906993cc3 Support to per-post display reactions 2023-06-07 18:50:29 -04:00
Vitor Pamplona
705309cb8a v0.55.4 2023-06-07 17:17:46 -04:00
Vitor Pamplona
89bdc9dd58 Adds support for RelaySet event types. 2023-06-07 17:16:48 -04:00
Vitor Pamplona
ac0103a53e v0.55.3 2023-06-07 15:57:31 -04:00
Vitor Pamplona
b2c31d3b46 Avoid reusing SHA256 object because of a memory leak in multithreading 2023-06-07 15:49:15 -04:00
Vitor Pamplona
931bd58c51 Removes main thread checks on Verification due to the need to mock this method when running tests. 2023-06-07 13:59:12 -04:00
Vitor Pamplona
6a7d6a843d Minor updates to remember states in composition 2023-06-07 12:26:32 -04:00
Vitor Pamplona
eaae672a30 Further breaks down the bottom bar to facilitate recompositions. 2023-06-07 12:25:49 -04:00
Vitor Pamplona
ccfbfeffe2 Moves ChatroomList's New Feed to an Additive filter. 2023-06-07 12:25:11 -04:00
Vitor Pamplona
eb62da8d26 Refactors the top bar relay updates. 2023-06-07 12:24:49 -04:00
Vitor Pamplona
d334c6b2e1 BugFix for the Notification dot not immediately disappearing after marking all as read. 2023-06-07 12:24:24 -04:00
Vitor Pamplona
90f0e4aad1 Refactoring duplicated code in Clickable Route 2023-06-07 12:23:30 -04:00
Vitor Pamplona
fabf52ffd7 Moves channel name to a remember state 2023-06-07 12:22:53 -04:00
Vitor Pamplona
c499c4baec Adds more main thread checks to make sure slow procedures are run outside the main thread. 2023-06-07 12:08:13 -04:00
Vitor Pamplona
4bd19c3e3d Deletes hiddenUsers method from account to avoid misuse since it needs a blocking load from cache. 2023-06-07 12:07:03 -04:00
Vitor Pamplona
6a47f5aa3b Moves notification's scroll to top animation to an IO thread. 2023-06-07 12:06:18 -04:00
Vitor Pamplona
03d1754498 Moves collection events to an IO thread. 2023-06-07 12:05:45 -04:00
Vitor Pamplona
e9374370b9 Moves author creation for Addressable notes to outside a @synchronized block to avoid deadlocks. 2023-06-07 12:04:50 -04:00
Vitor Pamplona
88a5e309bd Removes kinds from the filter since relays only allow 20 options. In this way, we get everything and filter it locally. 2023-06-07 12:03:44 -04:00
Zoltan
f7a292598e Adding one small Hungarian translation 2023-06-07 11:00:21 +02:00
Vitor Pamplona
cff8684347 updating firebase version 2023-06-06 16:47:28 -04:00
Vitor Pamplona
b9654d164f Further splitting the translation class into more methods to simplify recompositions 2023-06-06 16:47:14 -04:00
Vitor Pamplona
c10db10430 Moving unstable parameters into stable ones to reduce recompositions. 2023-06-06 15:51:43 -04:00
Vitor Pamplona
50f3f7f176 Fixes a search bug that wouldn't update relay filters correctly with the search terms 2023-06-06 15:50:21 -04:00
Vitor Pamplona
49c3334509 activates compose reports 2023-06-06 15:49:52 -04:00
Vitor Pamplona
e75f70985a upping memory requirements for the build 2023-06-06 15:49:29 -04:00
Vitor Pamplona
e956f51f19 v0.55.2 2023-06-05 18:22:26 -04:00
Vitor Pamplona
165678f118 formatting 2023-06-05 18:22:09 -04:00
Vitor Pamplona
d32d2da280 Increases the speed of the Zap Tab in Profiles. 2023-06-05 18:19:23 -04:00
Vitor Pamplona
d6a6a52821 Merge pull request #441 from davotoula/german_translation
Added German translation
2023-06-05 16:39:50 -04:00
Vitor Pamplona
dfb697975f v0.55.1 2023-06-05 16:06:32 -04:00
Vitor Pamplona
c0c0fd9e6d Fixes tests 2023-06-05 16:06:25 -04:00
Vitor Pamplona
5bdcdc6f4a - Refactoring feeds use of the pullrefresh
- Adding more places to check if in the main thread.
2023-06-05 15:33:16 -04:00
David Kaspar
102ab63673 Added German translation 2023-06-04 20:41:48 +01:00
Vitor Pamplona
b34c1f98d5 Marks some methods to never run on the Main Thread 2023-06-04 12:17:19 -04:00
Vitor Pamplona
74b05e3d6a Simplifying recompositions of NIP05 2023-06-04 12:17:02 -04:00
Vitor Pamplona
972bce426a No need to display replying information anymore. 2023-06-04 11:56:53 -04:00
Vitor Pamplona
3de57d6ae2 Moves memory cleanup to an IO thread 2023-06-04 11:56:39 -04:00
Vitor Pamplona
5695a82c20 Fixes: https://github.com/vitorpamplona/amethyst/issues/438 2023-06-04 11:45:34 -04:00
Vitor Pamplona
e171120836 Moves relay state to a new watcher 2023-06-03 21:46:26 -04:00
Vitor Pamplona
abdfe531f7 v0.55.0 2023-06-03 18:50:12 -04:00
Vitor Pamplona
ca90866877 Keeping the position of the feed in navigation 2023-06-03 17:02:09 -04:00
Vitor Pamplona
450fa2b70d Removing some unnecessary code 2023-06-03 16:21:02 -04:00
Vitor Pamplona
93c9ffeb02 adds a background to the app's icon in recommendation 2023-06-03 16:01:39 -04:00
Vitor Pamplona
2e5e546e7d Making sure video does not restart when pressing the mute button 2023-06-03 16:01:20 -04:00
Vitor Pamplona
00e7add001 Adds support for seeing NIP-89 recommendations on User's profile. 2023-06-03 12:39:06 -04:00
Vitor Pamplona
e28cd2212e v0.54.2 2023-06-02 22:46:45 -04:00
Vitor Pamplona
8f38353271 Shows comment and amount of zaps sent directly to the profile. 2023-06-02 22:45:58 -04:00
Vitor Pamplona
a41ee653d8 Moves reaction counts to a thread 2023-06-02 22:14:21 -04:00
Vitor Pamplona
839be06f62 Removes unnecessary mapping 2023-06-02 21:59:17 -04:00
Vitor Pamplona
bbef06de59 Moves UserFeed to MutableLists 2023-06-02 21:52:22 -04:00
Vitor Pamplona
515159f18c Ignores unloaded notes in the MultiComposeSet renderer. 2023-06-02 21:29:12 -04:00
Vitor Pamplona
e7b7ebbbdf Adds a new thread call for the notification of reaction rows 2023-06-02 21:28:50 -04:00
Vitor Pamplona
5965d9a3bb Moves profile watchers to their own compose functions 2023-06-02 21:28:30 -04:00
Vitor Pamplona
31959721b1 Moves reward calculations to the IO thread 2023-06-02 21:27:57 -04:00
Vitor Pamplona
8e16daaaa3 Only refreshes follow lists once per notification event. 2023-06-02 21:27:43 -04:00
Vitor Pamplona
e9fd47a536 Runs bottom's new notification refresher in the IO thread 2023-06-02 21:27:11 -04:00
Vitor Pamplona
6191ef9a6b Removes old notification cards 2023-06-02 20:36:01 -04:00
Vitor Pamplona
05aa12ebac - Removing outdated version of the FlowRow
- Improving performance of custom emojis
- Improving performance of DropDown menus
2023-06-02 20:30:49 -04:00
Vitor Pamplona
42428c5e0f Moves channel checks to happen before a Channel object is loaded. 2023-06-01 19:56:31 -04:00
Vitor Pamplona
1a3685c5ab v0.54.1 2023-06-01 16:07:08 -04:00
Vitor Pamplona
fff892e1c8 Making sure pollnote view models are unique 2023-06-01 16:03:22 -04:00
Vitor Pamplona
99c30816c6 Simplifying observables 2023-06-01 16:01:35 -04:00
Vitor Pamplona
92c61f317b Restructuring follows observables 2023-06-01 15:16:03 -04:00
Vitor Pamplona
b0e50e0c00 Fixes Translations 2023-06-01 09:11:47 -04:00
Vitor Pamplona
f065dcfc5a v0.54.0 2023-05-31 22:13:54 -04:00
Vitor Pamplona
91f34000f5 Improves rendering performance of the ExoPlayer 2023-05-31 22:09:18 -04:00
Vitor Pamplona
93e2af3c1f formatting 2023-05-31 21:32:53 -04:00
Vitor Pamplona
6072e5977e Reducing recompositions on images. 2023-05-31 21:32:46 -04:00
Vitor Pamplona
8575ea018d Improving rendering of the text 2023-05-31 20:43:57 -04:00
Vitor Pamplona
ed919ae531 UrlPreviewer using Main thread :( 2023-05-31 20:08:30 -04:00
Vitor Pamplona
3eb832c4e0 Improves Poll Rendering speed. 2023-05-31 20:08:07 -04:00
Vitor Pamplona
a57ca2a79d Using the correct immutable class 2023-05-31 16:33:23 -04:00
Vitor Pamplona
07a681c002 Merge remote-tracking branch 'origin/HEAD' 2023-05-31 16:31:55 -04:00
Vitor Pamplona
cb561d432c Merge pull request #433 from davotoula/add_per_app_language_preferences
Add support for per-app languange preferences
2023-05-31 16:31:19 -04:00
Vitor Pamplona
d942c12627 Restructuring ViewModels 2023-05-31 16:30:11 -04:00
David Kaspar
a466f33b4e Add support for per-app languange preferences 2023-05-31 09:34:21 +01:00
Vitor Pamplona
3744f90e87 Merge pull request #432 from davotoula/translation_sv
Added Swedish translation
2023-05-30 16:04:34 -04:00
Vitor Pamplona
1cc0f4bf80 Don't notify twice 2023-05-30 16:02:45 -04:00
David Kaspar
dec409c209 moved previous Swedish translation to a resource file
Corrected and added to Swedish translation
2023-05-30 20:58:50 +01:00
Vitor Pamplona
c664397b11 Starts video with the audio on/off based on global mute variable 2023-05-30 13:58:21 -04:00
Vitor Pamplona
bc1b71e9bf Improves reply info rendering 2023-05-30 13:58:00 -04:00
Vitor Pamplona
a42606af0d v0.53.7 2023-05-30 12:07:23 -04:00
Vitor Pamplona
b301e1d79b Removes sorting of the blocked list because it needs to load all metadata to sort correctly. 2023-05-30 12:07:15 -04:00
Vitor Pamplona
5ab94e8a1c - Renders the report's comment in the user profile report tab
- Allows reply, zap, boost and like the report itself.
- Shows report messages on notifications
2023-05-30 12:03:33 -04:00
Believethehype
ffa03e1d0d Merge remote-tracking branch 'upstream/main' 2023-05-30 09:17:15 +02:00
Vitor Pamplona
7ad982ec71 v0.53.6 2023-05-29 21:44:11 -04:00
Vitor Pamplona
030064e112 Sorting blocked users by name 2023-05-29 21:43:59 -04:00
Vitor Pamplona
d9a392d2c1 BugFix for not loading the security filter screen 2023-05-29 21:40:44 -04:00
Vitor Pamplona
97cd1c5e30 v0.53.5 2023-05-29 21:15:10 -04:00
Vitor Pamplona
24a6d8a155 Restart Video Cache when chancing Tor Setup 2023-05-29 21:14:06 -04:00
Vitor Pamplona
ea185147b3 v0.54.4 2023-05-29 19:47:02 -04:00
Vitor Pamplona
f2db2b88c2 Improves rendering of Markdown summaries 2023-05-29 19:46:54 -04:00
Vitor Pamplona
6ea0972151 Fixes bug with rendering of markdown with user citations. 2023-05-29 19:20:33 -04:00
Vitor Pamplona
093705f3b6 v0.52.3 2023-05-29 18:06:42 -04:00
Vitor Pamplona
1972297755 adds flavor to the version descriptor in the bottom left of the drawer 2023-05-29 18:06:35 -04:00
Vitor Pamplona
ac00694690 Moves ExoPlayer to OkHttp in order to enable Tor support. 2023-05-29 18:00:29 -04:00
Vitor Pamplona
605861dc32 v0.53.2 2023-05-29 17:12:04 -04:00
Vitor Pamplona
43d508ba33 Correcting the user's room for the notification marker 2023-05-29 17:11:56 -04:00
Vitor Pamplona
e692647d58 v0.53.1 2023-05-29 16:26:16 -04:00
Vitor Pamplona
64f98e4b92 Filters Message Feed DAL to only take PrivateMessages in the Notification Bubble. 2023-05-29 16:25:35 -04:00
Vitor Pamplona
c58c20cc54 Adding immutable tag to ResizeImage class 2023-05-29 15:15:20 -04:00
Vitor Pamplona
89ddb7f4b7 v0.53.0 2023-05-29 15:05:01 -04:00
Vitor Pamplona
cb92a51191 Adds separate colors for the newItem background between light and dark themes 2023-05-29 14:58:10 -04:00
Vitor Pamplona
213b172c32 Merge pull request #431 from davotoula/add_translation_cs
Add Czech translations
2023-05-29 14:27:01 -04:00
Vitor Pamplona
cfdd470626 formatting 2023-05-29 14:25:43 -04:00
Vitor Pamplona
a5a3c62edd Moving chatroomlist to an additive filter 2023-05-29 14:25:34 -04:00
David Kaspar
c9bcfa1c17 added Czech translation 2023-05-29 18:36:51 +01:00
Vitor Pamplona
02ad85a740 Avoids running through the entire filter when collection is Empty 2023-05-28 17:53:08 -04:00
Vitor Pamplona
446273de80 Adds mute control per track on the screen. 2023-05-28 17:36:20 -04:00
Vitor Pamplona
b513877552 Optimizing UserProfile status 2023-05-28 17:33:01 -04:00
Vitor Pamplona
013eedd892 Removing logs 2023-05-28 17:32:48 -04:00
Vitor Pamplona
d2cbb27756 Separating logs for full feed updates and additive feed updates 2023-05-28 17:32:40 -04:00
Vitor Pamplona
1a477ea1c8 Refreshing user status when added to the transient spam list 2023-05-28 17:32:05 -04:00
Vitor Pamplona
83df639aff Merge pull request #430 from malimahda/main
changes to translation
2023-05-28 16:25:10 -04:00
Vitor Pamplona
f298cc6f13 Adds some documentation to make sure any NIP05 changes take into account that we are converting the entire json to lowercase. 2023-05-28 16:24:37 -04:00
Vitor Pamplona
300a70ce18 Merge remote-tracking branch 'origin/HEAD' 2023-05-28 16:09:54 -04:00
Vitor Pamplona
0f6bf1cc59 Merge pull request #428 from davotoula/bugfix_case_sensitive_nip05
Bugfix case sensitive nip05
2023-05-28 16:09:37 -04:00
Vitor Pamplona
6bc1b6b28a Loading reply from launch and not withContent 2023-05-28 16:03:15 -04:00
Vitor Pamplona
1c381f64f9 v0.52.3 2023-05-28 15:35:46 -04:00
Vitor Pamplona
5e3703e849 Moves all note states to leaf compositors 2023-05-28 15:34:13 -04:00
Vitor Pamplona
acbd41ce61 Better recomposition structure for ReactionsRow 2023-05-28 15:17:10 -04:00
Vitor Pamplona
056d00b73b Performance Improvements when rendering a channel creation note 2023-05-28 15:16:50 -04:00
L
72fc21cdc6 Update strings.xml
translated new strings
2023-05-28 21:22:33 +03:30
L
3e983cdf1c Update strings.xml
added new strings
2023-05-28 20:40:16 +03:30
L
d81af9a194 Merge branch 'vitorpamplona:main' into main 2023-05-28 17:59:35 +03:30
David Kaspar
7f6b44cd0b spelling fix 2023-05-27 23:51:13 +01:00
David Kaspar
88bad1e308 added additional trivial tests 2023-05-27 22:35:34 +01:00
David Kaspar
aa0a4d1aac Use lowercase versions of user name and retrieved json to make case insensitive match 2023-05-27 21:56:57 +01:00
David Kaspar
b0a61cce42 Added failing test
(a bit dirty using a spy for the actual "system under test" but it is the quickest way to test added functionality and mocking out complexity around coroutines and httpclient)
2023-05-27 21:42:12 +01:00
Vitor Pamplona
b3be4567e2 v0.52.2 2023-05-27 16:36:36 -04:00
Vitor Pamplona
c5a586ec56 Fixes Missing Notifications 2023-05-27 16:36:27 -04:00
Vitor Pamplona
ece3664e74 Merge remote-tracking branch 'origin/HEAD' 2023-05-27 16:34:42 -04:00
Vitor Pamplona
2297c797dc Merge pull request #427 from ZsZolee/main
Sensitive content Hungarian translation
2023-05-27 16:24:48 -04:00
Vitor Pamplona
4517ca9fdd trimming urls as well 2023-05-27 16:16:52 -04:00
Vitor Pamplona
1146b8ad54 v0.52.1 2023-05-27 16:06:15 -04:00
Vitor Pamplona
545643d165 Fixes Relay Icon rendering. 2023-05-27 16:04:20 -04:00
Zoltan
359e22ae42 Sensitive content Hungarian translation 2023-05-27 17:02:15 +02:00
Vitor Pamplona
69be28ec98 Making sure the UserReactonsRow in notifications get updated as new events come in. 2023-05-26 18:05:35 -04:00
Vitor Pamplona
a8ffbb922d Moves relay color filters to a singleton 2023-05-26 14:03:57 -04:00
Vitor Pamplona
bd4a450336 v0.52.0 2023-05-26 12:21:10 -04:00
Vitor Pamplona
39818268ab Support for NIP-36, declaration of sensitive content. 2023-05-26 12:20:17 -04:00
Vitor Pamplona
d26b83a851 Updating kotlin dependencies. 2023-05-26 12:19:38 -04:00
Vitor Pamplona
8dc9c2ed2f 0.51.4 2023-05-26 09:37:51 -04:00
Vitor Pamplona
860f3bd096 Improves color contrast for Zap Amount notifications in light mode 2023-05-26 09:00:05 -04:00
Vitor Pamplona
a3adadb251 0.51.3 2023-05-25 21:09:47 -04:00
Vitor Pamplona
755d89dc79 Only changing stats for the Reaction Row if the data actually changed. 2023-05-25 21:09:14 -04:00
Vitor Pamplona
fa4463f1d7 Improves rendering of messages in the Notification feed 2023-05-25 21:08:20 -04:00
Vitor Pamplona
56a68b9100 Feed invalidation is necessary for lists 2023-05-25 20:40:04 -04:00
Vitor Pamplona
fe317964b8 Simplifying Private message rendering scheme 2023-05-25 20:39:15 -04:00
Vitor Pamplona
07ccf75d44 updates jsoup and activity compose to the latest versions 2023-05-25 18:43:01 -04:00
Vitor Pamplona
ff927d4830 v0.51.2 2023-05-25 18:38:34 -04:00
Vitor Pamplona
6461c3e498 Changes the structure of the notification view to increase speed. 2023-05-25 18:35:23 -04:00
Vitor Pamplona
2ed5eae029 Preserves invalidations of DataSources for Home, Videos and Notifications 2023-05-25 18:34:40 -04:00
Vitor Pamplona
1693455d0d Only removes scrollTop from Uri after 1 second and to avoid changing before composition finishes. 2023-05-25 18:33:22 -04:00
Vitor Pamplona
3bcda33fa6 Activates invalidations that can be ignored if another invalidation is already running. 2023-05-25 18:30:42 -04:00
Vitor Pamplona
9f394900a7 Only scrolls to the top if not at the top 2023-05-25 18:30:15 -04:00
Vitor Pamplona
4f2c78e73a Remembers formatting for the Notification Reaction Row 2023-05-25 18:29:41 -04:00
Vitor Pamplona
6a3600fd73 Updates Notification lists in intervals of 1 second. 2023-05-25 18:28:51 -04:00
Vitor Pamplona
b40c6f1361 remember modifiers to avoid recomposition 2023-05-25 18:28:34 -04:00
Vitor Pamplona
5ce19474ae Moves Home and Video screen invalidations to follow the scrollTop parameter 2023-05-25 18:28:19 -04:00
Vitor Pamplona
e9bdc045ba Adds @Stable tag to Notification Card models 2023-05-25 18:27:05 -04:00
Vitor Pamplona
aed7f46ca0 remembering size of the robohash picture 2023-05-25 18:25:47 -04:00
Vitor Pamplona
310a93ef29 removes logs 2023-05-25 16:24:16 -04:00
Vitor Pamplona
1ccaa1a028 Removing the start of relays before logging in. 2023-05-25 08:56:04 -04:00
Vitor Pamplona
493888d121 v0.51.1 2023-05-24 18:12:46 -04:00
Vitor Pamplona
e5aba62fdd Imports BahaUrlPreview library to add the proxy connection into it. 2023-05-24 18:11:51 -04:00
Vitor Pamplona
b643cedf8f Merge pull request #424 from KotlinGeekDev/retain-state
Retain state when rotating device.
2023-05-24 17:02:27 -04:00
Vitor Pamplona
2c0bfe2118 v0.51.0 2023-05-24 17:01:30 -04:00
Vitor Pamplona
f1ecaf1ee5 - Adds Immutable Lists to avoid recompositions
- Moves NavController to a lambda to avoid recompositions
- Moves AccountViewModel builder to a ViewModel Factory to avoid recompositions
- Reduces notifications invalidation requests when starting the screen.
2023-05-24 12:34:09 -04:00
Believethehype
719f5c2ac6 Merge remote-tracking branch 'upstream/main' 2023-05-24 17:30:10 +02:00
Vitor Pamplona
4841d4004e Fixes https://github.com/vitorpamplona/amethyst/issues/425 2023-05-24 10:38:25 -04:00
KotlinGeek
75d56214d2 Use manifest to avoid Activity recreation. 2023-05-23 23:58:59 +01:00
Vitor Pamplona
f0ba68e467 Fixing missing some notifications. 2023-05-23 16:04:29 -04:00
Vitor Pamplona
5a375b94d6 Adds Zap amounts to Notification bubbles. 2023-05-23 15:14:00 -04:00
Vitor Pamplona
dcbd4148e2 v0.50.6 2023-05-23 12:14:47 -04:00
Vitor Pamplona
0015100dea Fixing Immutable imports 2023-05-23 12:12:43 -04:00
Vitor Pamplona
8898e5d10b Adds Immutable and Stable annotations to avoid recomposing 2023-05-23 12:02:34 -04:00
Vitor Pamplona
15621f16a2 Improves the speed of updating Notification notes 2023-05-23 10:37:34 -04:00
Vitor Pamplona
a29d9ad433 Adds a cache layer for citations in Notes 2023-05-23 10:37:18 -04:00
Vitor Pamplona
d355f22938 Fixes click on the Home to update the feed 2023-05-23 10:35:56 -04:00
Vitor Pamplona
2082c6cb28 Fixes size for following symbol 2023-05-22 21:01:25 -04:00
Vitor Pamplona
4b49cc1aab better translations to unfollow in portuguese 2023-05-22 20:47:34 -04:00
Vitor Pamplona
538e61b38e rollback vm version 2023-05-22 20:47:13 -04:00
Vitor Pamplona
93f8847544 moving plugin to the latest 2023-05-22 20:41:42 -04:00
Vitor Pamplona
a1da4affeb Updating dependencies 2023-05-22 20:04:33 -04:00
Vitor Pamplona
0bf94f316f Moving more variables to remember clauses 2023-05-22 20:04:23 -04:00
Vitor Pamplona
a3fc22a04a v0.50.5 2023-05-22 18:24:50 -04:00
Vitor Pamplona
6437a2106a Moves zapProgress to inside the IO update to make sure it only updates when needed. 2023-05-22 17:52:46 -04:00
Vitor Pamplona
0a8c79dacd Stops recomposing the drawer at every new route. 2023-05-22 17:52:18 -04:00
Vitor Pamplona
22dbaaf95a Adjusts bottombar to not recalculate hasNewItems in every recomposition. 2023-05-22 17:51:59 -04:00
Vitor Pamplona
278091e6ef Moves default state of url previews to the remember function 2023-05-22 17:51:37 -04:00
Vitor Pamplona
d99711d3e6 Remembering Background color and Text elements for posts. 2023-05-22 17:51:12 -04:00
Vitor Pamplona
48f8cd7d95 Merge remote-tracking branch 'origin/HEAD' 2023-05-22 15:11:10 -04:00
Vitor Pamplona
3b816559e4 Optimizes MultiSetCompose 2023-05-22 15:10:54 -04:00
Vitor Pamplona
e96a50b454 Optimizes recomposition of bottom items. 2023-05-22 15:08:14 -04:00
Vitor Pamplona
93282b71ae Optimizes NIP-05 procedures 2023-05-22 15:07:47 -04:00
Vitor Pamplona
8223476cb3 Formatting 2023-05-22 15:07:21 -04:00
Vitor Pamplona
460187c4a4 Optimizes Drawer for Recomposition 2023-05-22 15:07:13 -04:00
Vitor Pamplona
5da49c920b Removes the time log for Composing a note 2023-05-22 14:55:06 -04:00
Vitor Pamplona
df5b5b90af Formatting 2023-05-22 14:54:42 -04:00
Vitor Pamplona
e5b937ed7e Only updates the refreshing state if it was refreshing. 2023-05-22 14:54:25 -04:00
Vitor Pamplona
b4b90cfd19 Merge pull request #420 from KotlinGeekDev/back-navigation-fix
Improve navigation behaviour.
2023-05-22 14:35:50 -04:00
Vitor Pamplona
99d302802a Deprecating IMGUR 2023-05-22 09:50:06 -04:00
Vitor Pamplona
ec2fc2f7e3 Updates relay UI on messages sent by the logged in user in chat 2023-05-22 09:49:34 -04:00
Vitor Pamplona
26634ccb04 Migrates notification summary to a new day without having to restart the app. 2023-05-22 09:18:13 -04:00
KotlinGeek
7766886587 Apply fix to profile navigation as well. 2023-05-22 12:56:17 +01:00
Vitor Pamplona
b219b3631c - Adds hashtags and #[n] rendering to markdown
- Shortcuts the link to Amethyst's router.
2023-05-21 20:54:42 -04:00
Vitor Pamplona
a0797f7104 Fixes NWC with Auth 2023-05-21 20:53:57 -04:00
Vitor Pamplona
330f3503a6 Adds nostr: prefix to channel id share. 2023-05-21 20:53:20 -04:00
KotlinGeek
7da87af0fb Verify presence of note in backstack before navigating to it. 2023-05-22 01:29:38 +01:00
Vitor Pamplona
2eb0096630 Adds a space in a bookmark author's name 2023-05-20 20:35:13 -04:00
L
459e579bcb Update strings.xml
changed some word to better choices
2023-05-20 20:46:04 +03:30
L
3c069d574e Merge branch 'vitorpamplona:main' into main 2023-05-20 20:38:19 +03:30
Vitor Pamplona
639569cd86 v0.50.4 2023-05-17 17:05:06 -04:00
Vitor Pamplona
e41df98920 Bugfix for channel metadata messages show up before channel creation packages 2023-05-17 17:01:07 -04:00
Vitor Pamplona
323e71c7cb Bugfix for saving Replaceable notes in Bookmarks. 2023-05-17 15:47:20 -04:00
Vitor Pamplona
cbe9462f28 v0.50.3 2023-05-17 13:55:30 -04:00
Vitor Pamplona
95febc1c8e Merge remote-tracking branch 'origin/main' 2023-05-17 13:55:17 -04:00
Vitor Pamplona
5d8ffd1e2c Merge pull request #415 from KotlinGeekDev/hashtag-search-fix
Fix for case-sensitive hashtag issue.
2023-05-17 13:55:02 -04:00
Vitor Pamplona
f2616855c2 Merge pull request #414 from ZsZolee/main
Replacing some missing Hungarian translation
2023-05-17 13:54:50 -04:00
Vitor Pamplona
5040350be5 Fixes Auth for NIP-42 sporadic connections. 2023-05-17 13:54:20 -04:00
KotlinGeek
d6c8651f0d Add lowercase variant of hashtag when creating event. 2023-05-17 17:26:31 +01:00
Zoltan
c5176b8c49 Replacing some missing Hungarian translation 2023-05-17 17:27:43 +02:00
Vitor Pamplona
d239020895 v0.50.2 2023-05-17 11:20:14 -04:00
Vitor Pamplona
117446688b Fixes jumping chat screens on hidden posts. 2023-05-17 11:20:03 -04:00
Vitor Pamplona
a8e650a38d v0.50.1 2023-05-17 09:50:43 -04:00
Vitor Pamplona
95a21cc08c Adds Navigation markers to the top panel and reaction watch 2023-05-17 09:49:31 -04:00
Vitor Pamplona
0823f2e7b5 Additive rendering of statistics 2023-05-17 09:34:21 -04:00
Vitor Pamplona
cb42196889 Adds reaction watch. 2023-05-16 22:41:47 -04:00
Vitor Pamplona
6aadf8a883 0.49.4 2023-05-16 19:00:44 -04:00
Vitor Pamplona
bb9fa8e144 Adding the missing search element 2023-05-16 18:51:08 -04:00
Vitor Pamplona
5a3cf40402 Adds online search when typing the username in the new Post field. 2023-05-16 18:43:58 -04:00
Vitor Pamplona
1519e70b24 Increasing size of the "More Options" button in the Video Feed. 2023-05-16 18:03:24 -04:00
Vitor Pamplona
135596591a Optimizes the code for chat bubble rendering 2023-05-16 17:53:36 -04:00
Vitor Pamplona
389afcb600 Fixes Robohash dependency in size 2023-05-16 17:53:15 -04:00
Vitor Pamplona
d6c2966792 Adds color to relay icons 2023-05-16 17:53:02 -04:00
Vitor Pamplona
dd8b208a0d Avoiding creating a lnurl link with just those words 2023-05-16 17:52:39 -04:00
Vitor Pamplona
27182a77a4 v0.49.3 2023-05-16 12:25:16 -04:00
Vitor Pamplona
cf5f345d54 Formatting 2023-05-16 12:20:35 -04:00
Vitor Pamplona
3950517743 Improves rendering performance for chats. 2023-05-16 12:20:27 -04:00
L
d2accf7622 Update strings.xml
a few words . not urgent
2023-05-16 19:22:04 +03:30
Vitor Pamplona
7a14a1ab26 Formatting 2023-05-16 11:30:54 -04:00
Vitor Pamplona
9e93a35ee7 Parsing NIP-19 to improve search results. 2023-05-16 11:30:43 -04:00
Vitor Pamplona
76df70c6f6 Making sure filters return similar orders when two posts are published at the same time. 2023-05-16 11:10:17 -04:00
Vitor Pamplona
1d5ff5bea9 v0.49.2 2023-05-16 10:24:45 -04:00
Vitor Pamplona
1767d4a3a3 Enabling a log to help NIP95 relay developers track what the app is doing with their images. 2023-05-16 10:24:05 -04:00
Vitor Pamplona
f2d87b78b1 Fixing emoji's on chat handles 2023-05-16 10:12:31 -04:00
Vitor Pamplona
4bf76d1b79 v0.49.1 2023-05-16 09:47:18 -04:00
Vitor Pamplona
17fbf8c01f BugFix for incorrect caching of NIP95 2023-05-16 09:47:10 -04:00
Vitor Pamplona
332490376b Adjustments to Markdown detection and rendering. 2023-05-16 08:52:59 -04:00
Vitor Pamplona
14e670cd9a Avoid color-filtering the emoji 2023-05-16 08:32:25 -04:00
Vitor Pamplona
d29e6b4d0b Better shade for Pins 2023-05-16 08:32:03 -04:00
Vitor Pamplona
3558bab786 v0.49.0 2023-05-15 22:18:21 -04:00
Vitor Pamplona
7c21f077db Support for PinStr (33888) 2023-05-15 22:18:12 -04:00
Vitor Pamplona
a2a9d04e1e v0.48.0 2023-05-15 21:35:55 -04:00
Vitor Pamplona
fc56f3988e Support for NIP-30 2023-05-15 21:26:59 -04:00
Vitor Pamplona
95ad33ace2 Merge pull request #412 from Pextar/main
Swedish translation
2023-05-15 14:11:11 -04:00
Vitor Pamplona
cf850415b8 Merge branch 'main' into main 2023-05-15 14:11:06 -04:00
Vitor Pamplona
bd77bb7780 Merge remote-tracking branch 'origin/HEAD' 2023-05-15 14:09:56 -04:00
Vitor Pamplona
6a696bfa39 Adds a 5 minute expiration time for ImageErrors 2023-05-15 14:09:43 -04:00
Pextar
d5fc97184c Merge branch 'main' into main 2023-05-15 20:07:43 +02:00
Pextar
e0b9120e17 Create values-sv 2023-05-15 20:06:08 +02:00
Vitor Pamplona
1523a29a4f Merge pull request #410 from greenart7c3/main
add longPress to edit tor/orbot settings
2023-05-15 13:51:25 -04:00
Vitor Pamplona
1db6b4793f Adds NIP-94 shareable link to filestr 2023-05-15 13:25:21 -04:00
Vitor Pamplona
426a495eb4 Adds zapstr.live and listr.lol as shareable links 2023-05-15 13:17:31 -04:00
Vitor Pamplona
e07b4d49ef Fixes NPE on opening the QuickAction on a "Post Not Found" event. 2023-05-15 13:10:33 -04:00
Vitor Pamplona
595cab3630 v0.47.0 2023-05-15 11:16:50 -04:00
Vitor Pamplona
817a9c3e8d Adds support for AudioTrack Types from (https://zapstr.live/) 2023-05-15 11:15:32 -04:00
greenart7c3
32927040e3 add longPress to edit tor/orbot settings 2023-05-15 11:17:42 -03:00
Vitor Pamplona
c973d941cc v0.46.6 2023-05-14 22:23:37 -04:00
Vitor Pamplona
db6ebe609c Forces aspect ratio while obeying maxWidth and Height of the frame. 2023-05-14 22:22:53 -04:00
Vitor Pamplona
32913915ce Making sure remember does not interfere with translations. 2023-05-14 22:22:22 -04:00
Vitor Pamplona
4a8680e582 0.46.5 2023-05-14 21:06:06 -04:00
Vitor Pamplona
7cf0291b5a Applies remember functions to more parameters. 2023-05-14 21:05:58 -04:00
Vitor Pamplona
0406632e9d Moves internal content changes to derived states. 2023-05-14 20:24:41 -04:00
Vitor Pamplona
bf2949717c Makes sure to only change the visible status when needed. 2023-05-14 20:24:11 -04:00
Vitor Pamplona
bbea28c6df 0.46.4 2023-05-14 19:04:34 -04:00
Vitor Pamplona
1c76b7a35c Testing a not cropping version for videos. 2023-05-14 18:59:49 -04:00
Vitor Pamplona
814d05812a Fixes image cropping opening the image dialog. 2023-05-14 18:59:33 -04:00
Vitor Pamplona
b4d3cf87c8 Merge remote-tracking branch 'origin/HEAD' 2023-05-14 18:21:43 -04:00
Vitor Pamplona
e5ff92c804 v0.46.3 2023-05-14 18:21:33 -04:00
Vitor Pamplona
c3a31fd313 Update PRIVACY.md
Adds the collection of the push notification token to the Privacy.MD
2023-05-14 18:20:28 -04:00
Vitor Pamplona
3fd4c176e8 Merge remote-tracking branch 'origin/HEAD' 2023-05-14 18:12:16 -04:00
Vitor Pamplona
cf9395d5e3 Avoids Notification duplicates 2023-05-14 18:12:04 -04:00
Vitor Pamplona
89ae229ea2 Merge pull request #408 from vivganes/intl-hashtags
fix #407 correct non-english hashtags
2023-05-14 17:37:18 -04:00
Vitor Pamplona
f40b6c394c v0.46.2 2023-05-14 17:22:22 -04:00
Vitor Pamplona
f84501e40f Improvements to Poll Caching system: Fixes https://github.com/vitorpamplona/amethyst/issues/406 2023-05-14 17:03:46 -04:00
vivganes
d261003217 fix #407 correct non-english hashtags 2023-05-14 23:39:42 +05:30
Pextar
fc1ee54548 Merge branch 'vitorpamplona:main' into main 2023-05-14 08:29:09 +02:00
Vitor Pamplona
6842d12b39 Turn FollowList into a ViewModel and a LocalCache LiveData into a SharedFlow (it was killing some coroutines that were supposed to finish) 2023-05-13 12:49:48 -04:00
Vitor Pamplona
3e69b81d81 Moves aspect ratio to the appropriate composable 2023-05-13 12:46:57 -04:00
Vitor Pamplona
ea4e3e75d6 Formatting 2023-05-13 12:46:34 -04:00
Vitor Pamplona
5db04ef2b0 Allows the RichTextViewer to render before resolving urls. 2023-05-13 12:46:11 -04:00
Vitor Pamplona
b51186de30 updated secp lib 2023-05-12 17:18:56 -04:00
Vitor Pamplona
8fc0f5a8b8 Removing shadowed variable 2023-05-12 16:50:26 -04:00
Pextar
754ac9894b Delete values-sv 2023-05-12 20:38:08 +02:00
Vitor Pamplona
69744db53b Merge remote-tracking branch 'origin/HEAD' 2023-05-12 14:17:31 -04:00
Vitor Pamplona
db9a0ebfa1 Protects against exceptions when creating keys with invalid bytearrays. 2023-05-12 14:13:08 -04:00
Vitor Pamplona
38ebeded1c Merge pull request #402 from ZsZolee/main
TOR Hungarian translation
2023-05-12 13:40:41 -04:00
Vitor Pamplona
2b8fafc123 Merge pull request #403 from judemont/main
French translation for Android description.
2023-05-12 13:40:24 -04:00
Vitor Pamplona
fc2408b526 Correcting NPE from failing to load the thumbnail 2023-05-12 13:32:08 -04:00
JdM
6723806534 French translation for android description. 2023-05-12 17:26:11 +02:00
Zoltan
93c3a49ecf TOR Hungarian translation 2023-05-12 14:04:52 +02:00
Pextar
e80c8b37b3 Create values-sv 2023-05-12 12:14:20 +02:00
Vitor Pamplona
66b84c0dea 0.46.1 2023-05-11 18:30:18 -04:00
Vitor Pamplona
a7063042fb Formatting 2023-05-11 18:15:56 -04:00
Vitor Pamplona
aa60367c72 Doesn't update if it returned the same reference for the list. 2023-05-11 18:15:48 -04:00
Vitor Pamplona
19a58bb669 correctly refreshes the screen 2023-05-11 17:39:31 -04:00
Vitor Pamplona
55aafffb37 Remove logs 2023-05-11 17:39:21 -04:00
Vitor Pamplona
9b92dfca5e Isolating state changes in the profile page. 2023-05-11 17:31:32 -04:00
Vitor Pamplona
3ae75a3333 Adds FAB column to create new channels and join channels/users 2023-05-11 16:42:05 -04:00
Vitor Pamplona
9ebb3fbefc Improving reply screen to clarify which users will be notified. 2023-05-11 15:26:09 -04:00
Vitor Pamplona
96d66646b4 Displaying lists as Notes 2023-05-11 14:39:05 -04:00
Vitor Pamplona
f202422ff1 hasValidSignature crashes in some weirdly formed events. 2023-05-11 10:12:01 -04:00
Vitor Pamplona
f02e18ff9b Fix background color from remember calls 2023-05-11 09:32:33 -04:00
Vitor Pamplona
b72b0b028f v0.46.0 2023-05-11 09:19:27 -04:00
Vitor Pamplona
eb9be2108d Adjustments to Tor/Orbot screens 2023-05-11 09:18:17 -04:00
Vitor Pamplona
875eb0df78 Merge pull request #357 from greenart7c3/main
Add option to setup connection through Orbot
2023-05-11 08:33:33 -04:00
Vitor Pamplona
ea8a665bc7 Merge pull request #401 from Radiokot/fix/actualize_ru_uk_23_05_10
Actualize Russian and Ukrainian translations
2023-05-11 08:33:11 -04:00
Vitor Pamplona
142f850eb7 Merge pull request #400 from Bardesss/patch-5
Update strings.xml (NL)
2023-05-11 08:33:04 -04:00
Vitor Pamplona
9f12fffd0d Merge pull request #399 from malimahda/main
values-fa/strings
2023-05-11 08:32:46 -04:00
Vitor Pamplona
28a1f0d6da v0.45.1 2023-05-10 19:36:07 -04:00
Vitor Pamplona
b1de36f423 Trying to improve rendering by allowing recompositions to perform layout faster. 2023-05-10 19:08:56 -04:00
Oleg Koretsky
ff54d581ae Actualize Russian and Ukrainian translations 2023-05-10 22:57:41 +03:00
Vitor Pamplona
8fcd05a6bb avoids cutting the first paragraph of notifcations 2023-05-10 14:03:05 -04:00
Vitor Pamplona
434c1e9af3 Don't try to decrypt if message is blank 2023-05-10 14:02:53 -04:00
Vitor Pamplona
a62f2e44d0 Only calls invalidate if things change 2023-05-10 14:02:38 -04:00
Vitor Pamplona
aa0ace5014 Delaying a bit more the average update filter for relays. 2023-05-10 14:02:05 -04:00
greenart7c3
ee4c54aa27 use httpclient.getHttpClient 2023-05-10 14:47:46 -03:00
greenart7c3
7ff408bfb9 Merge branch 'main' into main 2023-05-10 14:42:37 -03:00
Vitor Pamplona
c1d127ac1e Fixing blank messages and Zapped DM content 2023-05-10 13:31:25 -04:00
Vitor Pamplona
0b2e5de752 Uppercase 2023-05-10 13:30:52 -04:00
Vitor Pamplona
920ce54fe3 v0.45.0 2023-05-10 11:20:28 -04:00
Vitor Pamplona
c87b246a4e Fixing tests 2023-05-10 11:19:32 -04:00
Vitor Pamplona
b8bc370bc1 Support for Push Notifications in the PlayStore build 2023-05-10 11:16:47 -04:00
L
a8069f7a3e Update strings.xml 2023-05-10 16:17:01 +03:30
L
ca9b7adfa0 Update strings.xml
a few words
2023-05-10 15:21:40 +03:30
L
3c3b6544cf Update strings.xml
changed a couple of words
2023-05-10 15:02:28 +03:30
Bardesss
910849b3c7 Update strings.xml (NL)
Updated translation
2023-05-10 08:55:43 +00:00
L
411ba0f137 Merge pull request #1 from believethehype/persian_translation
fixed path to Persian translation file
2023-05-09 23:02:36 +03:30
Believethehype
d48a15bfb3 Update strings.xml 2023-05-09 21:15:55 +02:00
Believethehype
489941106b fixed path of persian translation 2023-05-09 21:14:50 +02:00
Believethehype
a0391fb8a5 Merge branch 'pr/1' into persian_translation 2023-05-09 21:11:40 +02:00
Believethehype
baa3a21dd2 Merge remote-tracking branch 'upstream/main' 2023-05-09 21:04:08 +02:00
Vitor Pamplona
633be54dd4 Updates dependencies 2023-05-09 13:20:59 -04:00
Vitor Pamplona
470281fa87 Filters lists out of the user's profile 2023-05-09 12:58:56 -04:00
Vitor Pamplona
3287196243 updating gradle plugin 2023-05-09 11:10:53 -04:00
Vitor Pamplona
bb8bb16370 Better regex for nostr:n.. citations in text. 2023-05-09 11:10:39 -04:00
Vitor Pamplona
4325975686 v0.44.0 2023-05-08 16:20:40 -04:00
Vitor Pamplona
53ec9d777f - Moving postValue to IO thread
- launching navigate in a thread
2023-05-08 16:10:16 -04:00
Vitor Pamplona
3e6e7d4863 - Checks if the file still exists before discarding a duplicate event.
- Adding a per list EOSE
2023-05-08 15:34:59 -04:00
Vitor Pamplona
81290f2b26 Moving follows to the known list. 2023-05-08 14:58:39 -04:00
Vitor Pamplona
4dcf38c492 Fixing tab sliders from jumping from one state to the other 2023-05-08 14:46:23 -04:00
Vitor Pamplona
04c1300317 Adding List Choice to Notifications 2023-05-08 11:04:37 -04:00
greenart7c3
0d051117b2 add toast message to strings.xml 2023-05-08 11:03:17 -03:00
greenart7c3
d31eca1b42 Merge branch 'main' into main 2023-05-08 10:48:48 -03:00
greenart7c3
a503a1b218 show orbot dialog at login 2023-05-08 10:45:10 -03:00
Vitor Pamplona
70890def82 v0.43.2 2023-05-08 09:43:31 -04:00
Vitor Pamplona
d0eadcf30d Formatting 2023-05-08 09:39:54 -04:00
Vitor Pamplona
ae268cc197 Fixing NostrFiles dev downloader 2023-05-08 09:39:44 -04:00
L
0bf317a818 Update strings.xml 2023-05-08 12:01:21 +03:30
L
915f7c07e8 Update strings.xml 2023-05-08 10:40:59 +03:30
L
69a1302404 Update strings.xml 2023-05-08 09:47:05 +03:30
Vitor Pamplona
d6d273e34c v0.43.1 2023-05-07 21:46:01 -04:00
Vitor Pamplona
462c73341f Fixes cutting text in the wrong language 2023-05-07 21:45:38 -04:00
Vitor Pamplona
aa07fbfc94 Adjusting NostrFilesDev to the latest API 2023-05-07 18:47:39 -04:00
Vitor Pamplona
59d7f7cc70 Merge pull request #396 from believethehype/nostrfilesdev
Added Nostrfiles.dev Image hoster Backend Code
2023-05-07 18:42:39 -04:00
Vitor Pamplona
950eefa4e3 Merge branch 'main' into nostrfilesdev 2023-05-07 18:42:09 -04:00
Vitor Pamplona
f2bbdd44a6 Removing file transform function 2023-05-07 18:40:24 -04:00
Vitor Pamplona
cb09a26ef5 Refactoring the nostr.build uploader 2023-05-07 18:37:33 -04:00
L
43156e3e11 Update strings.xml 2023-05-07 23:51:47 +03:30
Vitor Pamplona
eba14cf9f1 v0.43.0 2023-05-07 16:14:18 -04:00
Vitor Pamplona
13de5973ca Improving performance of the RichText Viewer 2023-05-07 16:12:28 -04:00
Vitor Pamplona
936f750826 Moving private messages and channel loading to a thread. 2023-05-07 16:11:40 -04:00
L
faf215110a Update strings.xml 2023-05-07 23:34:27 +03:30
L
839e00ae36 Update strings.xml 2023-05-07 22:50:37 +03:30
L
b829a42d76 Update strings.xml 2023-05-07 22:45:58 +03:30
L
e1c3191f31 Update strings.xml 2023-05-07 22:18:25 +03:30
Vitor Pamplona
f44549de41 Loading Users and Notes in Launched Effects only because of @Synchronized in the getOrCreate methods in LocalCache 2023-05-07 12:51:12 -04:00
Vitor Pamplona
1c0fcc4ab8 Solves NPE when loading an invalid npub in a citation 2023-05-07 12:50:17 -04:00
Vitor Pamplona
fcc6793244 Limiting AuthorGalleries to 50 people 2023-05-07 08:53:52 -04:00
Vitor Pamplona
dea35552d5 Calculating ZapAmount and converting to the screen before exiting the IO thread 2023-05-06 22:27:00 -04:00
Vitor Pamplona
b093873eb0 Avoids trying to decrypt a zap that was not sent by the author. 2023-05-06 22:17:43 -04:00
Vitor Pamplona
e5de2c45de Moving withContext to scope.launch to avoid waiting the results. 2023-05-06 21:58:25 -04:00
Vitor Pamplona
70d8d9e2e0 formatting 2023-05-06 20:59:24 -04:00
Vitor Pamplona
dc0fe75b3d Removing crossfades that might reduce the loading speed 2023-05-06 20:59:09 -04:00
Vitor Pamplona
25a34dd2e8 formatting 2023-05-06 20:03:04 -04:00
Vitor Pamplona
90227e1cdc Refactoring NIP05 verification 2023-05-06 20:02:54 -04:00
Vitor Pamplona
bba45dd0a7 Moving clickable to Row and not AsyncImage because it's faster 2023-05-06 19:19:57 -04:00
Vitor Pamplona
fdca0af97b Improving performance of the UserNameDisplay by making variables immutable 2023-05-06 19:18:15 -04:00
Vitor Pamplona
795120e932 Improving performance of Badge events 2023-05-06 19:17:39 -04:00
Vitor Pamplona
79da9cee01 NoteCompose refactoring 2023-05-06 11:49:53 -04:00
Vitor Pamplona
fbbb9b0d72 Increasing sensitivity for Language identification options 2023-05-06 09:53:44 -04:00
Vitor Pamplona
97d56d61d1 Caching Private Zap decryption 2023-05-06 09:46:25 -04:00
Vitor Pamplona
f3316f53ea Renames Hex.toByteArray() to avoid confusing with String.toByteArray() 2023-05-06 09:24:39 -04:00
Vitor Pamplona
e3ccf71a84 v0.42.5 2023-05-05 18:50:19 -04:00
Vitor Pamplona
635279d59a Testing removing the gradient of quoted posts. 2023-05-05 18:49:32 -04:00
Vitor Pamplona
0c673e91c6 BugFix for displaying other people's list after loading them from their profile 2023-05-05 18:49:13 -04:00
Vitor Pamplona
3519e20a98 BugFix for the empty selection 2023-05-05 18:48:37 -04:00
Vitor Pamplona
7167d57d22 v0.42.4 2023-05-05 13:49:41 -04:00
Vitor Pamplona
86adac95c9 Adds protections for calculating zap amounts without a private key 2023-05-05 13:49:18 -04:00
Vitor Pamplona
cffcc158f5 0.42.3 2023-05-05 13:29:20 -04:00
Vitor Pamplona
49a8778975 BugFix for NPE on null ZappedNote 2023-05-05 13:26:55 -04:00
Believethehype
dd62d2bae4 activated nostrfiles_dev for stories feed. 2023-05-05 19:25:31 +02:00
Believethehype
3c3b6c97c0 deactivate nostrfiles_dev selection for now (because of space) 2023-05-05 19:21:25 +02:00
Believethehype
dabeea43ea added hoster nostrfiles.dev 2023-05-05 18:59:24 +02:00
Believethehype
8cb84c2395 Merge remote-tracking branch 'upstream/main' 2023-05-05 17:52:38 +02:00
Vitor Pamplona
5b6b6f19e5 Reverting background refactoring due to color being stuck in recompositions. 2023-05-05 10:01:37 -04:00
Vitor Pamplona
28370dbc01 Merge pull request #395 from ZsZolee/main
Follow list Hungarian translation
2023-05-05 09:11:03 -04:00
greenart7c3
28521c1a60 add configuration screen when enabling tor in the drawer 2023-05-05 08:25:48 -03:00
greenart7c3
9c30547bd7 fix build 2023-05-05 06:34:14 -03:00
greenart7c3
b07fd253c7 Merge branch 'main' into main 2023-05-05 06:11:24 -03:00
Zoltan
ab841c6490 Follow list Hungarian translation 2023-05-05 10:38:53 +02:00
Vitor Pamplona
12ff434a07 v0.42.2 2023-05-04 22:35:08 -04:00
Vitor Pamplona
51432c8c50 Fixing the toByteArray call 2023-05-04 22:34:09 -04:00
Vitor Pamplona
483e7d673c Refactoring part of NoteCompose 2023-05-04 21:57:19 -04:00
Vitor Pamplona
3ce6ce9951 v0.42.1 2023-05-04 21:14:55 -04:00
Vitor Pamplona
84f51dc473 Removing animation from blurhash 2023-05-04 21:14:42 -04:00
Vitor Pamplona
5f4886ce52 Fixes migration of follow lists from old accounts 2023-05-04 21:10:09 -04:00
Vitor Pamplona
dea6f0ca21 v0.42.0 2023-05-04 20:10:01 -04:00
Vitor Pamplona
01bdc41286 Merge remote-tracking branch 'origin/HEAD' 2023-05-04 20:08:50 -04:00
Vitor Pamplona
8c2e89197e Adds support for Private and Public Follow Lists 2023-05-04 20:08:36 -04:00
Vitor Pamplona
712950c2a1 Merge pull request #393 from believethehype/hashtag_fixes
Update HashtagIcon.kt
2023-05-04 11:49:38 -04:00
Believethehype
95cc2ee589 Update HashtagIcon.kt 2023-05-04 13:29:00 +02:00
Vitor Pamplona
76283463b0 v0.41.0 2023-05-03 18:26:52 -04:00
Vitor Pamplona
5179a821b2 Adjustments to the image upload choice to make sure they fit in a usual screen. 2023-05-03 18:25:01 -04:00
Vitor Pamplona
974a8011f9 Merge remote-tracking branch 'origin/main' 2023-05-03 17:49:14 -04:00
Vitor Pamplona
771bb3fc27 Merge pull request #391 from believethehype/nostrbuild
Added example for Image upload on nostr.build
2023-05-03 17:48:50 -04:00
Vitor Pamplona
10bd63ca2f Merge pull request #392 from Bardesss/patch-4
Update strings.xml (NL)
2023-05-03 17:44:29 -04:00
Vitor Pamplona
1fad3d1c8a Testing the removal of the image proxy server. 2023-05-03 17:20:10 -04:00
Believethehype
67cc24c4d9 added nostr.build NIP94 2023-05-03 18:13:18 +02:00
Vitor Pamplona
4368b34569 Fixes tall images bleeding into other posts. 2023-05-03 10:21:52 -04:00
greenart7c3
264d6a40b3 increased the default timeout when using proxy 2023-05-03 09:00:14 -03:00
Bardesss
65540c8a95 Update strings.xml (NL)
Updated translations
2023-05-03 08:13:35 +00:00
Bardesss
b801aa6bd1 Update strings.xml (NL)
Updated translation
2023-05-03 08:08:15 +00:00
Believethehype
7224ad4b05 image upload works to nostr.build 2023-05-03 00:48:01 +02:00
Believethehype
ee52e725d9 Merge remote-tracking branch 'upstream/main' 2023-05-03 00:35:41 +02:00
Believethehype
b043e2da2c Merge remote-tracking branch 'upstream/main' into nostrbuild 2023-05-03 00:27:39 +02:00
Vitor Pamplona
f8a60203ec Adds NFC support for NIP19 URIs 2023-05-02 17:59:12 -04:00
Vitor Pamplona
7f18bc3a88 0.40.6 2023-05-02 17:11:56 -04:00
Vitor Pamplona
31740d8855 Adding Post Not found on NIP95 images that are not found in any relay. 2023-05-02 17:06:32 -04:00
Vitor Pamplona
8641e0fec5 Only showing the loading url after 200 ms to avoid blinking the text. 2023-05-02 17:04:06 -04:00
Vitor Pamplona
c8f1e13448 Formatting 2023-05-02 16:47:02 -04:00
Vitor Pamplona
580cf76577 BugFix on calculating the Zap amount. 2023-05-02 16:46:52 -04:00
Vitor Pamplona
61549ebfd3 BugFix for video saving tool 2023-05-02 16:03:04 -04:00
Vitor Pamplona
797d1bb2b4 Don't try to draw local image if the file does not exist 2023-05-02 15:53:44 -04:00
Vitor Pamplona
99c16879c1 Uses resizeMode according to context. 2023-05-02 15:53:15 -04:00
Vitor Pamplona
0bf80e88ce Avoids videos getting out of the current page and leaking into the next page. 2023-05-02 15:45:32 -04:00
Vitor Pamplona
a71679ef43 Removing the exposure of the URL from the initial image and video loads. 2023-05-02 15:44:59 -04:00
Vitor Pamplona
4bbb34e8c9 v0.40.5 2023-05-02 14:38:18 -04:00
Vitor Pamplona
0e1420a626 Adjust volume based on changing global mute variable. 2023-05-02 14:38:09 -04:00
Vitor Pamplona
79782ee8e7 Fixes the volume adjusting interfering with other apps. 2023-05-02 14:32:49 -04:00
Vitor Pamplona
81b70780a6 v0.40.4 2023-05-02 12:42:38 -04:00
Vitor Pamplona
277fc5971e Moving Home feed to start first when loading the app. 2023-05-02 12:31:03 -04:00
Vitor Pamplona
850bc15f4f More visible buttons in the Video feed. 2023-05-02 12:20:05 -04:00
Vitor Pamplona
6d2db65c12 Basic Mp3 Support 2023-05-02 11:48:38 -04:00
Vitor Pamplona
89ce6dbab2 Forces search types to be used by the search components and reorganizes search 2023-05-02 11:39:29 -04:00
Vitor Pamplona
80bfbf68b4 Sending dimensions to Image files. 2023-05-02 10:02:45 -04:00
Vitor Pamplona
4464247ddb NVK is beautiful. 2023-05-02 08:20:45 -04:00
Vitor Pamplona
486d43dc1b 0.40.3 2023-05-01 20:44:37 -04:00
Vitor Pamplona
6731ca59cd BugFix for mixing NIP94 and NIP95 selections 2023-05-01 20:43:31 -04:00
Vitor Pamplona
7d620cd89a BugFix for now showing the error message when the phone can't open the file 2023-05-01 20:43:14 -04:00
Vitor Pamplona
82a2586eab Adds language to warn that NIP94 and 95 are new and other clients might not see it. 2023-05-01 20:42:48 -04:00
Vitor Pamplona
5a3f912b73 Merge pull request #389 from vivganes/translation-ta
Add tamil translations
2023-05-01 20:06:56 -04:00
Vitor Pamplona
bc54134e65 0.40.2 2023-05-01 18:54:51 -04:00
Vitor Pamplona
0f080219b2 Starts on mute 2023-05-01 18:54:06 -04:00
Vitor Pamplona
ff54629601 Autoplay controls 2023-05-01 18:53:07 -04:00
Believethehype
f889ff2547 added nostrbuild, can only upload very small files right now 2023-05-02 00:23:59 +02:00
Believethehype
f3e0c31087 Merge remote-tracking branch 'upstream/main' 2023-05-02 00:13:32 +02:00
Vitor Pamplona
8f257a7e2d - Displays Video Uploading Progress
- Keeps position in the Video feed between screen
- Animates to the top when posting
- Adds NIP-95 in-disk caching
- Fixes the Crossfade for BlurHash on NIP94 and NIP95 events
2023-05-01 16:42:58 -04:00
Vitor Pamplona
9015089593 Correctly loading NIP95 events from relays. 2023-05-01 16:39:07 -04:00
Vitor Pamplona
2b87930283 Removing the need to prune NIP95 events. 2023-05-01 16:38:35 -04:00
Believethehype
92ccdfc55f Merge remote-tracking branch 'upstream/main' 2023-05-01 18:59:38 +02:00
vivganes
15d94ca9c4 Add tamil translations 2023-05-01 21:15:55 +05:30
Vitor Pamplona
44af7f958c Merge pull request #388 from vitorpamplona/zap_payments
Zap payments without zap services on the receiving end.
2023-05-01 11:22:01 -04:00
Vitor Pamplona
2b2028853e Finishes the payment database 2023-05-01 11:12:46 -04:00
Vitor Pamplona
66dcc03351 Merge remote-tracking branch 'origin/HEAD' into zap_payments 2023-05-01 08:55:52 -04:00
Believethehype
0528e59572 Merge remote-tracking branch 'upstream/main' 2023-05-01 13:42:26 +02:00
Believethehype
0ab3219115 Merge branch 'main' of https://github.com/believethehype/amethyst 2023-05-01 13:42:05 +02:00
Vitor Pamplona
22ba150c4e v0.40.1 2023-04-30 21:35:53 -04:00
Vitor Pamplona
63cc00976f Fixes https://github.com/vitorpamplona/amethyst/issues/387 2023-04-30 21:34:19 -04:00
Vitor Pamplona
cc9d4c237a Fixes Bug with the Nostr's Report use of double asterisk 2023-04-30 21:31:37 -04:00
Vitor Pamplona
446135ea2a Adding Zap Payments to the calculation of amounts and zapped by 2023-04-30 21:30:30 -04:00
Vitor Pamplona
a38bbd7168 Adding hex index for the Video Feed 2023-04-30 17:00:47 -04:00
Vitor Pamplona
635ed1afc1 v0.40.0 2023-04-29 19:45:27 -04:00
Vitor Pamplona
130ad40a3a Merge pull request #385 from vitorpamplona/tiktok
Tiktok
2023-04-29 19:44:36 -04:00
Vitor Pamplona
be5e58111e Merge remote-tracking branch 'origin/HEAD' 2023-04-29 19:31:12 -04:00
Vitor Pamplona
8b012884b1 0.39.0 2023-04-29 19:31:00 -04:00
Vitor Pamplona
4949a5c459 Merge pull request #384 from ZsZolee/main
Upload server Hungarian translation
2023-04-29 19:30:06 -04:00
Vitor Pamplona
ab330fea96 v0.39.0 2023-04-29 19:29:42 -04:00
Vitor Pamplona
8209c45303 New Video Feed based on NIP-94 and 95 2023-04-29 19:28:46 -04:00
Zoltan
206935289e Upload server Hungarian translation 2023-04-29 10:45:16 +02:00
Vitor Pamplona
571ff0e72b v0.38.0 2023-04-28 18:42:44 -04:00
Vitor Pamplona
940914adf5 Merge remote-tracking branch 'origin/HEAD' 2023-04-28 18:40:32 -04:00
Vitor Pamplona
1496f012a5 Adds new Highlight event kind from https://highlighter.com/ 2023-04-28 18:40:12 -04:00
greenart7c3
20a65a0878 check if orbot is installed 2023-04-28 11:21:06 -03:00
greenart7c3
6285bfaae7 updated string.xml 2023-04-28 08:15:04 -03:00
Vitor Pamplona
2a552bea4d Update PRIVACY.md
Clarifying that the PRIVACY policy covers the distribution usage and not the source code usage.
2023-04-27 19:30:20 -04:00
Vitor Pamplona
d1dc06a467 v0.37.4 2023-04-27 18:29:04 -04:00
Vitor Pamplona
f173f3194b Adjusts size of the Zap Forward text. 2023-04-27 18:27:56 -04:00
Vitor Pamplona
8ca63f32f4 Avoids flickering in the Global Feed 2023-04-27 18:20:04 -04:00
Vitor Pamplona
6b3e10ed28 Version 0.37.3 2023-04-27 17:37:23 -04:00
Vitor Pamplona
1ae785061e Reverting compose to 1.4.2 2023-04-27 17:35:53 -04:00
Vitor Pamplona
ddba4ac510 Add logs for fail to decrypt zap 2023-04-27 17:35:36 -04:00
Vitor Pamplona
0fe6bd6c83 Bugfix for clickable url 2023-04-27 17:13:56 -04:00
Vitor Pamplona
925289ae64 Reducing the amount of filters to send to relays to 10 2023-04-27 17:13:45 -04:00
Vitor Pamplona
f6f82a16f5 v0.37.2 2023-04-27 16:23:38 -04:00
Vitor Pamplona
54511dcc69 Updating dependencies 2023-04-27 16:23:30 -04:00
Vitor Pamplona
2f457d135f Improving translations 2023-04-27 15:54:26 -04:00
Vitor Pamplona
362082d608 Correcly copy pasting NIP94 and NIP95 addresses. 2023-04-27 15:40:28 -04:00
Vitor Pamplona
78cfa98456 Merge pull request #381 from vivganes/non-english-hashtags
Add support for non-english hashtags too
2023-04-27 15:28:38 -04:00
Vitor Pamplona
5e37a97d1e Merge branch 'main' into non-english-hashtags 2023-04-27 15:28:08 -04:00
Vitor Pamplona
561189144e v0.37.1 2023-04-27 15:06:04 -04:00
Vitor Pamplona
285a61e38f Enabling non NIP94 uploads for the PlayStore 2023-04-27 15:01:58 -04:00
Vitor Pamplona
2ca877b3c3 Zap Forwarding 2023-04-27 14:43:28 -04:00
vivganes
f105188738 Add support for non-english hashtags too 2023-04-27 21:13:25 +05:30
Vitor Pamplona
0a11de1892 Adds Zap Address preference to the zap tag https://github.com/nostr-protocol/nips/pull/402/files 2023-04-27 09:27:29 -04:00
Vitor Pamplona
1596a7b559 Pruning file storage events when pausing the app. 2023-04-27 09:26:26 -04:00
Vitor Pamplona
d4a3ee2651 Adding nip95 to readme 2023-04-26 19:04:01 -04:00
Vitor Pamplona
c60ad472e8 v0.37.0 2023-04-26 18:05:19 -04:00
Vitor Pamplona
04e4c4692f disabling logs 2023-04-26 18:05:02 -04:00
Vitor Pamplona
ab747f5e93 Activating NIP-95 support 2023-04-26 18:04:38 -04:00
Vitor Pamplona
e370e75ba4 Offers NostrImg as a choice of image server 2023-04-26 16:36:52 -04:00
Vitor Pamplona
a669e37774 Adds server selection support 2023-04-26 14:23:06 -04:00
Vitor Pamplona
86fe9b4a65 Adds support for NIP-95 Images 2023-04-26 14:22:49 -04:00
Vitor Pamplona
c6a0b0950a Fixes AUTH NIP42 bug. Amethyst was sending the event as a regular EVENT and not an AUTH command 2023-04-26 13:51:22 -04:00
greenart7c3
77a1b2ba89 show a dialog when disabling tor 2023-04-26 14:41:36 -03:00
Believethehype
724cfddb69 Merge branch 'main' of https://github.com/believethehype/amethyst 2023-04-26 19:22:44 +02:00
Vitor Pamplona
f03cd04566 Merge pull request #380 from ZsZolee/main
Zap Type Hungarian translation
2023-04-26 13:02:01 -04:00
greenart7c3
3217d5095b created initProxy 2023-04-26 11:54:04 -03:00
greenart7c3
5d9d7ba16e created getProxy function 2023-04-26 10:19:30 -03:00
greenart7c3
c648f54b51 created HttpClient class 2023-04-26 10:03:51 -03:00
greenart7c3
4f8d33d850 fix build 2023-04-26 08:51:58 -03:00
greenart7c3
9f3d6a75e6 add proxy to all http clients 2023-04-26 08:42:49 -03:00
Zoltan
3175d40482 Zap Type Hungarian translation 2023-04-26 11:08:13 +02:00
Vitor Pamplona
24d722cdb7 v0.36.0 2023-04-25 21:19:32 -04:00
Vitor Pamplona
14bc1fb7d4 Support for NIP-42 2023-04-25 21:18:33 -04:00
Vitor Pamplona
90955515d0 Updating readme with the new features. 2023-04-25 18:26:18 -04:00
Vitor Pamplona
a22ec35a26 v0.35.1 2023-04-25 18:16:51 -04:00
Vitor Pamplona
f38f5737b0 Opens image dialog in the page of the current selection. 2023-04-25 18:11:08 -04:00
Vitor Pamplona
e958515dad Improves configuration of Zap types (double click and click and hold) 2023-04-25 18:06:00 -04:00
Vitor Pamplona
add6b06446 Marks all Zaps to be private by default. 2023-04-25 17:13:07 -04:00
Vitor Pamplona
d070712f2b Activates gif preview in Notifications 2023-04-25 17:12:48 -04:00
Vitor Pamplona
b9f58b3701 Correctly updating the screen after zaps have been accounted for 2023-04-25 17:02:44 -04:00
Vitor Pamplona
91f3f036ae Making poll votes work with Private Zaps. 2023-04-25 16:35:42 -04:00
Vitor Pamplona
4ef90e26cf Merge pull request #377 from ZsZolee/main
New Hungarian translation
2023-04-25 14:27:13 -04:00
Vitor Pamplona
f7182d6393 Preview for quote noting. 2023-04-25 13:37:39 -04:00
Vitor Pamplona
99861facc9 Improving the performance and UI of Zap notification messages. 2023-04-25 09:21:12 -04:00
Vitor Pamplona
a5728daeb0 Adds FileHeader loading to a thread. 2023-04-25 08:20:51 -04:00
Zoltan
a4ac2eeb89 New Hungarian translation 2023-04-25 12:07:01 +02:00
Vitor Pamplona
a5a320b889 Merge pull request #376 from kiwiidb/main
fix bug in nip47 scheme check
2023-04-25 05:08:13 -04:00
kiwiidb
8f927c29e3 fix bug in nip47 scheme check 2023-04-25 10:15:07 +02:00
Vitor Pamplona
a739b27d14 v0.35.0 2023-04-24 21:04:01 -04:00
Vitor Pamplona
2f2ca97ec1 Updating dependencies. 2023-04-24 21:03:01 -04:00
Vitor Pamplona
114186ce06 Merge pull request #373 from vitorpamplona/new_wallet_connect
New wallet connect API
2023-04-24 18:55:40 -04:00
Vitor Pamplona
8187a7f5a9 removing debugging log 2023-04-24 18:54:19 -04:00
Vitor Pamplona
f8319c720a Support for the new JSON-based Wallet Connect API 2023-04-24 18:51:57 -04:00
Vitor Pamplona
3d93fe3d18 Merge remote-tracking branch 'origin/HEAD' into new_wallet_connect 2023-04-24 18:02:24 -04:00
Vitor Pamplona
b4373ae1ed Deprecating #[]s tags 2023-04-24 17:58:35 -04:00
greenart7c3
ad420da6de added tor icon 2023-04-24 16:37:00 -03:00
Vitor Pamplona
a37effd42d Merge pull request #372 from believethehype/privatezaps
Update for Custom Zaps
2023-04-24 10:25:47 -04:00
Vitor Pamplona
655cd20a01 Adds support for video and image descriptions for accessibility use cases 2023-04-24 10:21:01 -04:00
greenart7c3
a354fe928b Merge branch 'main' of github.com:greenart7c3/amethyst 2023-04-24 08:16:55 -03:00
Believethehype
a13dbf2bda decode private zaps and show in notifications + show zap messages 2023-04-23 22:48:53 +02:00
Vitor Pamplona
f1affc2dbb Formatting 2023-04-23 16:32:50 -04:00
Vitor Pamplona
cbc0f95498 Show previews of most notes, including NIP94 - images, on new posts. 2023-04-23 16:32:44 -04:00
Vitor Pamplona
b3f8b06fea formatting improvements 2023-04-23 16:10:20 -04:00
Vitor Pamplona
ed4c19ac79 Adjusts the Wallet Connect API to the new RPC design 2023-04-23 16:10:09 -04:00
Believethehype
37bd7e34a0 remember default ZapType
Based on last selection in ZapCustomDialog, Reaction Row Zaps will use the same zap type

Currently Zaps in Polls are always public.
2023-04-23 18:53:38 +02:00
Believethehype
7fcc28a320 Merge branch 'main' of https://github.com/believethehype/amethyst 2023-04-23 18:09:28 +02:00
Vitor Pamplona
c1d05f8b2f Adds some margin for reply quotes 2023-04-22 15:14:00 -04:00
Vitor Pamplona
9c715dd381 Fixing the position of the image loading animation 2023-04-22 15:02:33 -04:00
Vitor Pamplona
a720a636d3 Merge pull request #348 from believethehype/private_zaps
Send Private Zaps
2023-04-22 14:52:06 -04:00
Vitor Pamplona
5322d6e900 Merge pull request #367 from jltdhome/main
Added Adaptive Icon
2023-04-22 14:50:42 -04:00
Vitor Pamplona
9d849f12c0 Merge pull request #368 from believethehype/hashtags_release
New HashTag Icons #footstr #grownostr
2023-04-22 14:49:17 -04:00
Believethehype
18dc5354bd removed unused graphics 2023-04-22 20:13:23 +02:00
Believethehype
a013f1f1cc added new hashtag icons 2023-04-22 20:11:05 +02:00
Believethehype
e1d236d9cf Merge remote-tracking branch 'upstream/main' into hashtags_release 2023-04-22 20:04:04 +02:00
Josh
12a2a21c0e Added Adaptive Icon
Take advantage of themed icons in Android 13+
2023-04-21 21:06:19 -07:00
Believethehype
9ae13a9f97 Update LnZapRequestEvent.kt 2023-04-22 05:54:43 +02:00
Believethehype
bd64a7de63 clean up 2023-04-22 05:50:38 +02:00
Believethehype
12a3cd5113 Update LocalCache.kt 2023-04-22 05:37:52 +02:00
Believethehype
1d23e87bd9 refactoring 2023-04-22 05:32:48 +02:00
Believethehype
b23bea8971 Merge remote-tracking branch 'upstream/main' into private_zaps 2023-04-22 05:06:59 +02:00
Believethehype
94051f178b Merge remote-tracking branch 'upstream/main' 2023-04-22 04:06:40 +02:00
Vitor Pamplona
830ade9ad6 v0.34.1 2023-04-21 18:22:11 -04:00
Vitor Pamplona
ed07248f7d Correctly calculates the total of Zaps in a poll 2023-04-21 18:20:26 -04:00
Vitor Pamplona
9aa5993748 Removes unnecessary logs 2023-04-21 18:20:15 -04:00
Vitor Pamplona
e7909beaab Remove spaces after bench and image previews 2023-04-21 18:20:00 -04:00
Vitor Pamplona
79e3450a46 v0.34.0 2023-04-21 17:07:07 -04:00
Vitor Pamplona
783204b57f Support for NIP-94 2023-04-21 17:01:42 -04:00
Vitor Pamplona
1adb2b2caa Fix crashing when changing users. 2023-04-21 11:00:06 -04:00
Vitor Pamplona
3f579793ca v0.33.2 2023-04-21 10:21:50 -04:00
Vitor Pamplona
8040608e14 Fixes: https://github.com/vitorpamplona/amethyst/issues/359 2023-04-21 10:21:24 -04:00
Believethehype
2d202e2244 resolving conflicts with latest main branch. 2023-04-21 15:48:04 +02:00
Believethehype
8e4f339aa1 Merge remote-tracking branch 'upstream/main' 2023-04-21 15:21:24 +02:00
Vitor Pamplona
2e66d278ad Merge pull request #362 from vivganes/patch-361
Show clickable phone number and nostr:npub reference in markdown notes
2023-04-21 08:48:50 -04:00
Vitor Pamplona
1bfbf3643d Merge branch 'main' into patch-361 2023-04-21 08:48:41 -04:00
Vitor Pamplona
5990bf0213 Merge pull request #360 from Bardesss/patch-3
Update strings.xml (NL)
2023-04-21 08:44:33 -04:00
vivganes
5e3db99675 fix #361 2023-04-21 17:20:50 +05:30
Bardesss
a291741c37 Update strings.xml 2023-04-21 09:06:54 +00:00
Bardesss
94ee80d8ea Update strings.xml 2023-04-21 08:59:36 +00:00
Bardesss
f7de66c6a5 Update strings.xml (NL)
Added missing strings.
2023-04-21 07:34:42 +00:00
Vitor Pamplona
48d890b354 v0.33.1 2023-04-20 17:24:33 -04:00
Vitor Pamplona
c7a9bd1226 Using FlowRow to display relay icons. 2023-04-20 17:19:34 -04:00
Vitor Pamplona
c40c7bc62b Removing LazyGrid from reactions Row due to being slower than the Row itself. 2023-04-20 17:19:20 -04:00
Vitor Pamplona
cca7a10322 Caching images that cause errors and avoid calling them again. 2023-04-20 17:18:53 -04:00
Vitor Pamplona
fe6ef1df0a Migration to Gradle 8.0 2023-04-20 17:18:20 -04:00
greenart7c3
cf8747b19f add a checkbox to use tor in the login screen, set the proxy in the account model construction 2023-04-20 15:30:04 -03:00
Vitor Pamplona
3e27567532 Moves home and Notification bubbles to the adaptive feed. 2023-04-20 14:09:26 -04:00
Vitor Pamplona
0a15e079dc Compares room messages by reference. 2023-04-20 14:08:58 -04:00
Vitor Pamplona
48f4fc4a0f v0.33.0 2023-04-20 12:11:58 -04:00
Vitor Pamplona
d9396ce453 Migrating to the default Pager from Accompanist's library. 2023-04-20 10:55:00 -04:00
Vitor Pamplona
817c5172b6 Clearing some warnings up 2023-04-20 10:54:35 -04:00
Vitor Pamplona
cfdb20892e Updating dependencies 2023-04-20 10:53:09 -04:00
Vitor Pamplona
75922f93bc Moving ReactionsRow into a Lazy Grid 2023-04-20 10:09:28 -04:00
Vitor Pamplona
42801e3eca Using the URL Detector (which is faster) to catch urls in the text 2023-04-20 10:08:32 -04:00
Vitor Pamplona
64321dbe7c No need to sort. It will be resorted later. 2023-04-20 10:07:34 -04:00
Vitor Pamplona
b11b52b60c typo 2023-04-20 10:07:10 -04:00
Vitor Pamplona
00e5470642 Passing the size of the robohash rendering to the request in the hopes to improve performance. 2023-04-20 10:07:03 -04:00
Vitor Pamplona
7ee3b8a196 Improves performance of video loading and playback. 2023-04-20 10:06:12 -04:00
greenart7c3
2ae54b544f wip: add tor socks support 2023-04-20 08:21:11 -03:00
Vitor Pamplona
585bb57f64 Merge pull request #356 from vitorpamplona/additive_feed_test
Additive feeds (No more list reloadings in the main screens)
2023-04-19 16:49:37 -04:00
Vitor Pamplona
6981fe8f8a Adapting interfaces for the additive filter. 2023-04-19 16:35:26 -04:00
Vitor Pamplona
abdad7fbea solve recursive method call 2023-04-19 15:57:27 -04:00
Vitor Pamplona
57b35398be Activates updates 2023-04-19 15:57:17 -04:00
Vitor Pamplona
47398e6e86 Starts with an empty set as database 2023-04-19 15:56:58 -04:00
Vitor Pamplona
e775ae9ada Adding the new parameter. 2023-04-19 13:54:00 -04:00
Vitor Pamplona
6a9a321e2b Fixing format 2023-04-19 13:00:52 -04:00
Vitor Pamplona
0aee4407db Merge remote-tracking branch 'origin/HEAD' into additive_feed_test
# Conflicts:
#	app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt
#	app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt
#	app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt
2023-04-19 13:00:45 -04:00
Vitor Pamplona
a816fa561a Declares the use of the camera as a feature. 2023-04-19 11:34:38 -04:00
Vitor Pamplona
dc745c3cdb disables errors due to missing translations 2023-04-19 11:33:03 -04:00
Vitor Pamplona
0b30c83143 Moves coffee.xml to the right directory. 2023-04-19 11:32:52 -04:00
Vitor Pamplona
dc8804933d moving source code compatibility to java 17 2023-04-19 11:27:20 -04:00
Vitor Pamplona
b28f0bff74 Updating build scripts to java 17 2023-04-19 11:24:49 -04:00
Vitor Pamplona
e96e3c0760 Merge pull request #353 from ryogrid/quote_problem
fix bug: quote repost is tied up to a post which is replyed a little while ago
2023-04-19 10:38:40 -04:00
Vitor Pamplona
b7ea167420 Merge branch 'main' into quote_problem 2023-04-19 10:38:20 -04:00
Vitor Pamplona
744a55f497 Caching channel info JSON parse in NoteCompose. 2023-04-19 10:34:10 -04:00
Vitor Pamplona
b55f999efb Slight performance improvements on tag search. 2023-04-19 10:33:52 -04:00
Vitor Pamplona
4e09d9e54a Performance Improvements for Zaps in Polls. 2023-04-19 10:33:02 -04:00
Vitor Pamplona
5afa333fab Migration to Android Studio 2022.2.1 and Gradle 8 2023-04-19 09:58:11 -04:00
Ryo Kanbayashi
c0946f5c0e fix bug: quote repost is tied up to a post which is replyed a little while ago. (2) 2023-04-19 22:50:32 +09:00
Vitor Pamplona
3ece4fcf8b Merge pull request #352 from naderbal/drawer_back
Intercept back press when drawer is opened to close drawer
2023-04-18 17:10:15 -04:00
Vitor Pamplona
b1193c5d11 Merge pull request #344 from ZsZolee/main
Added new Hungarian translations
2023-04-18 17:09:43 -04:00
Vitor Pamplona
12b0afd9c6 Display note IDHex when measuring time. 2023-04-18 08:46:12 -04:00
Vitor Pamplona
df45d68b74 Caches user profile in Account. 2023-04-18 08:45:50 -04:00
Vitor Pamplona
11bf702e02 Moves DropDown state to the IO thread. 2023-04-18 08:45:34 -04:00
Vitor Pamplona
47e2683aa3 Moves firstIsTaggedHash search to the IO thread. 2023-04-18 08:45:02 -04:00
Vitor Pamplona
e4fc00669e Compares users by reference. 2023-04-18 08:44:32 -04:00
Vitor Pamplona
f64db1271b Uses createdAt instead of the card object, which can change randomly, to determine when to call LaunchedEffects. 2023-04-18 08:44:11 -04:00
Vitor Pamplona
35647a8361 Fixes DM link in Message Notifications 2023-04-18 08:42:58 -04:00
Vitor Pamplona
a2c8658972 Only calculates nip05 after identifying if it is correct. 2023-04-18 08:42:32 -04:00
Vitor Pamplona
38e18cf0a7 Simplifies comparison for Channels via Hex code instead of objects 2023-04-18 08:41:59 -04:00
Vitor Pamplona
78c9974036 Removes invalidateFilters Logging 2023-04-18 08:41:29 -04:00
Believethehype
7a60a6804a Update LnZapEventInterface.kt 2023-04-17 20:14:17 +02:00
Believethehype
5e6e2835ca Receiving and decoding private Zaps
Private zaps get decoded correctly.

TODO: Update events or at least notifications in Damus to replace anon sender with the one from decrypted message. Bonus: Show Content of the Zap Message
2023-04-17 20:10:36 +02:00
Ryo Kanbayashi
180083147d removed needless added newline. 2023-04-16 22:26:55 +09:00
Ryo Kanbayashi
50b0b5af37 fix bug: quote repost is tied up to a post which is replyed a little while ago. 2023-04-16 22:12:54 +09:00
nalbaltaji
9d27b8d47e Intercept back press when drawer is opened to close drawer 2023-04-15 18:59:44 +02:00
Believethehype
fbb4dc5e59 additional hashtags 2023-04-13 15:35:37 +02:00
Believethehype
a2296f2365 Update grownostr.xml 2023-04-13 14:38:20 +02:00
Believethehype
58ff2e6157 Update HashtagIcon.kt 2023-04-13 00:19:20 +02:00
Believethehype
ff490a2b14 Send Private Zaps 2023-04-13 00:15:36 +02:00
Believethehype
067d694036 some more community hashtags 2023-04-12 20:49:32 +02:00
Believethehype
425aa69691 Merge branch 'main' of https://github.com/believethehype/amethyst 2023-04-12 19:08:46 +02:00
Zoltan
401d807d29 Added new Hungarian translations 2023-04-11 10:26:19 +02:00
Vitor Pamplona
eab202f721 Merge pull request #340 from toadlyBroodle/main
#zappoll hashtag
2023-04-10 10:07:13 -04:00
Vitor Pamplona
f726c656b5 Merge pull request #343 from believethehype/zaptypes
introduce zap types, implemented non-zaps in custom zap dialog
2023-04-10 10:03:35 -04:00
Believethehype
37dab1c8b6 Update LnZapEvent.kt 2023-04-10 13:54:29 +02:00
Believethehype
9a5d8522fb added anonymous zaps 2023-04-10 13:25:09 +02:00
Believethehype
5abbd07722 introduce zap types, implemented non-zaps in custom zap dialog
Non-Zaps allow to tip the author of a note without using the zap mechanism (no record on the note)
2023-04-09 23:43:36 +02:00
Believethehype
1c65334b91 Delete footstr.xml 2023-04-09 23:39:57 +02:00
Believethehype
726d04f0f4 Update HashtagIcon.kt 2023-04-09 23:39:32 +02:00
Believethehype
9d1ab5a9b8 introduce zap types, implemented non-zaps in custom zap dialog
Non-Zaps allow to tip the author of a note without using the zap mechanism (no record on the note)
2023-04-09 23:37:23 +02:00
Believethehype
09bad79870 #footstr 2023-04-09 05:31:31 +02:00
toadlyBroodle
13579f9473 add #zappoll to new poll messages 2023-04-09 11:26:02 +09:00
Vitor Pamplona
cea9cdcd34 v0.32.3 2023-04-07 18:33:36 -04:00
Vitor Pamplona
ed22155940 Simplifying tag structure 2023-04-07 18:30:34 -04:00
Vitor Pamplona
43497f2ce0 fixes format 2023-04-07 18:26:19 -04:00
Vitor Pamplona
80e58f72f8 Fixes nprofile1 tags 2023-04-07 18:26:13 -04:00
Vitor Pamplona
234751a4b3 Avoids loading markdown style when it won't be used. 2023-04-07 18:11:50 -04:00
Vitor Pamplona
ef567110d0 Moving processing flags to Launched Effect. 2023-04-07 18:02:54 -04:00
Vitor Pamplona
42b89f438b Only calculates follows when needed. 2023-04-07 17:44:40 -04:00
Vitor Pamplona
b01df2b38e Fixes MultiSet Compose Routes 2023-04-07 17:41:00 -04:00
Vitor Pamplona
b89efb547e Correctly points Users to the right DM when clicking in the notification. 2023-04-07 17:38:25 -04:00
Vitor Pamplona
e6554a5b4a Moves tags calculation to Launched Effect 2023-04-07 17:38:02 -04:00
Vitor Pamplona
c58b13e2f7 Simplifies modifiers 2023-04-07 17:37:25 -04:00
Vitor Pamplona
241372ce61 Adds DMs to the id search filter 2023-04-07 17:37:07 -04:00
Vitor Pamplona
49ed594f53 Caches logged user in Note compose 2023-04-07 17:21:21 -04:00
Vitor Pamplona
03b76357ff Migrates poll and invoice option availability to a mutable state 2023-04-07 17:20:18 -04:00
Vitor Pamplona
97490bb6ef Using bitcoin icon instead of dollar. 2023-04-07 17:19:47 -04:00
Vitor Pamplona
dbf5267c5c Moves LnInvoice, Tags, BechLinks and Previews to LaunchedEffect 2023-04-07 16:58:25 -04:00
Vitor Pamplona
e1ce638d7f Compares by reference to simplify code 2023-04-07 16:56:20 -04:00
Vitor Pamplona
88b80a0da1 Typo 2023-04-07 16:56:08 -04:00
Vitor Pamplona
d6b571590a Sends simpler object to help compose's stability 2023-04-07 16:55:46 -04:00
Vitor Pamplona
83f3af340f Uses Kotlin's rounding function instead of java 2023-04-07 16:55:15 -04:00
Vitor Pamplona
e9cf9b21a8 Merge pull request #339 from toadlyBroodle/main
Prevent author vote manipulation
2023-04-07 14:43:00 -04:00
Vitor Pamplona
b1645590fe Merge pull request #336 from wklkejw239/patch-1
add contributors in README.md
2023-04-07 14:41:48 -04:00
Vitor Pamplona
a5da4ea53d Streamlines Global filter for performance 2023-04-07 14:26:23 -04:00
Vitor Pamplona
3a082d131f Streamlines Notification filter for performance 2023-04-07 14:26:07 -04:00
Vitor Pamplona
28d501ed02 Uses more direct functions to get and compare tags as opposed to the fail safe options. 2023-04-07 14:25:38 -04:00
Vitor Pamplona
a04fd4a9f2 Matches parameter names to the Event class. 2023-04-07 14:25:03 -04:00
Vitor Pamplona
2424dc3961 Brings the findCitedUsers function to BaseTextNote Event with cache. 2023-04-07 14:24:43 -04:00
Vitor Pamplona
58f1515387 Displays feed performance measurement before clipping to 1000 objects 2023-04-07 14:24:00 -04:00
Vitor Pamplona
9ee3da3910 Uses the inHidden in Hex 2023-04-07 14:23:34 -04:00
Vitor Pamplona
cc44b4dd55 Removes reposts from conversations 2023-04-07 14:23:19 -04:00
Vitor Pamplona
b8e4ccfc28 Lint mentions cannot be null. 2023-04-07 14:22:48 -04:00
Vitor Pamplona
0fc6757d4a Adds inHidden check for hex keys 2023-04-07 14:22:16 -04:00
toadlyBroodle
ee94cc6bf6 don't allow poll authors to vote in their own polls 2023-04-07 08:41:34 +09:00
Vitor Pamplona
f224fe6b40 v0.32.2 2023-04-06 12:23:35 -04:00
Vitor Pamplona
403a1723de Previews Notes that are not a citation using #[n] 2023-04-06 11:59:21 -04:00
Vitor Pamplona
6cf1960df4 Adds a New Message Tagger to Chats (tags Notes and Users by default) and Private Message (doesn't tag Users and Notes) 2023-04-06 11:58:49 -04:00
Vitor Pamplona
8676752f19 Fixes Channel's reply to filtration (removes citations and the channel id from replies) 2023-04-06 11:57:28 -04:00
Vitor Pamplona
7012d949c2 Fixes formatting 2023-04-06 11:56:23 -04:00
Vitor Pamplona
8344274011 Adds Invoice Creation to New Posts. 2023-04-06 11:54:28 -04:00
vincivermeer
2bda50ad4a add contributors in README.md
Hello everyone!

I added the contributors in the readme.md file in the repository: vitorpamplona/amethyst. I would like to contribute to this pull-request and I hope you all accept it.

what do you all think of this idea?
2023-04-06 00:11:49 -03:00
Vitor Pamplona
b0953310c2 Making the poll click animation wider. 2023-04-05 11:10:14 -04:00
Vitor Pamplona
65910295db Adds notification for Zaps that don't have a Note attached to them. 2023-04-05 08:46:25 -04:00
Vitor Pamplona
7a957bd6ec Initializing indexes with large sizes from the start. 2023-04-05 08:45:58 -04:00
Vitor Pamplona
292ce779a1 Lazy initialization of the json mapper (300ms) 2023-04-05 08:45:33 -04:00
Vitor Pamplona
924b21cdfc Reducing the use of Slow disk access of Local Preferences. 2023-04-05 08:44:48 -04:00
Vitor Pamplona
7b539e63bd Merge pull request #334 from Poussinou/patch-1
Update README.md
2023-04-05 08:21:06 -04:00
Poussinou
d37fef94fe Update README.md 2023-04-05 21:24:47 +09:30
Vitor Pamplona
93a1853317 Merge pull request #332 from braydonf/readme
Update readme gradlew install commands
2023-04-04 19:48:16 -04:00
Braydon
9944f98bde Update readme gradlew install commands 2023-04-04 16:24:44 -07:00
Vitor Pamplona
05138232f4 Fixes missing function 2023-04-04 16:12:20 -04:00
Vitor Pamplona
83d7085375 Hides Poll option when replying to channel or DMs. 2023-04-04 16:10:55 -04:00
Vitor Pamplona
a8a3915c8d Allowing replies of PMs from Notifications. 2023-04-04 16:09:54 -04:00
Vitor Pamplona
83bcf53583 v0.32.1 2023-04-04 14:11:05 -04:00
Vitor Pamplona
875d9544ec Fixes Poll Color for Light mode. 2023-04-04 13:59:02 -04:00
Vitor Pamplona
7ce2a0bbea Makes sure before and after voting the padding is the same. 2023-04-04 13:57:06 -04:00
Vitor Pamplona
a3b9ea6a42 Fixes format 2023-04-04 13:51:27 -04:00
Vitor Pamplona
ca2c811859 Fixes Voted option highlight. 2023-04-04 13:49:41 -04:00
Vitor Pamplona
b09bb0c3c5 Fixes margins when images are used in the poll 2023-04-04 13:39:12 -04:00
Vitor Pamplona
a68ea1abe5 Adds margin between the reply note and the text 2023-04-04 12:35:42 -04:00
Vitor Pamplona
02a9aaab60 Removes the 5dp space on the generic RichText Viewer 2023-04-04 12:29:25 -04:00
Vitor Pamplona
efd2ddba71 Adds payment animation to Polls 2023-04-04 12:25:32 -04:00
Vitor Pamplona
e10c5771d8 v0.32.0 2023-04-04 11:27:23 -04:00
Vitor Pamplona
b685b38565 Avoids triggering exceptions when the NIP19 parser receives a simple hex key 2023-04-04 11:21:36 -04:00
Vitor Pamplona
15822fee8e Fixes Translation bug not being active on google play edition 2023-04-04 11:20:57 -04:00
Vitor Pamplona
e96e218025 Adds relay param in spam filter for polls 2023-04-03 20:46:22 -04:00
Vitor Pamplona
a11263ce9e Correcting format 2023-04-03 20:42:05 -04:00
Vitor Pamplona
4417201582 Merge pull request #330 from vitorpamplona/pr/321
Moving new pool button to NewPost
2023-04-03 20:35:41 -04:00
Vitor Pamplona
6a85ab743d Moving new pool button to NewPost 2023-04-03 20:31:25 -04:00
Vitor Pamplona
4434b0e0e5 Merge pull request #321 from toadlyBroodle/main
Polls
2023-04-03 20:26:57 -04:00
Vitor Pamplona
7c8381624b Merge branch 'main' into main 2023-04-03 20:26:50 -04:00
Vitor Pamplona
e470531210 Adds Show QR option to Profile screen 2023-04-03 15:04:52 -04:00
Vitor Pamplona
182cf62a3b v0.31.4 2023-04-03 14:02:19 -04:00
Vitor Pamplona
3e666df896 Adds full nip19 parsing on key decoding capabilities. 2023-04-03 14:01:43 -04:00
Vitor Pamplona
20aca144fa Moves Login QR Button to a leading QR, not a trailing QR 2023-04-03 14:01:00 -04:00
Vitor Pamplona
25d9f7b529 Merge pull request #327 from believethehype/main
added message to LnZapEvent and interface
2023-04-03 13:07:48 -04:00
Vitor Pamplona
306a7b3041 Merge pull request #329 from greenart7c3/main
Add qrCode scanner to the login page
2023-04-03 13:07:08 -04:00
greenart7c3
bae1b6c6e6 Merge branch 'main' of github.com:greenart7c3/amethyst 2023-04-03 13:55:06 -03:00
greenart7c3
f2311f294e Merge branch 'vitorpamplona:main' into main 2023-04-03 13:54:48 -03:00
greenart7c3
ad2eabf386 add qrcode scanner to login page 2023-04-03 13:54:13 -03:00
greenart7c3
b51f551b4b fix formatting 2023-04-03 13:49:08 -03:00
Vitor Pamplona
af751d56ce Hunting a bug of missing zapRequests 2023-04-03 12:03:00 -04:00
Vitor Pamplona
f4d66d9cbf Speeding up RichText rendering parsers 2023-04-03 12:02:26 -04:00
Vitor Pamplona
37d839504c Avoiding crashes when zap description is blank 2023-04-03 11:53:02 -04:00
Believethehype
e8e696dcea added message to LnZapEvent and interface 2023-04-02 23:25:37 +02:00
Vitor Pamplona
4db0faeda4 Printing time on EOSETime class for logs 2023-04-02 11:54:40 -04:00
Vitor Pamplona
b8ca1a15fb Adds event kind to the NoteCompose log 2023-04-02 11:43:15 -04:00
Vitor Pamplona
407ee74a55 Faster processing of Contact List events 2023-04-02 11:42:38 -04:00
Vitor Pamplona
67bcc52b25 Faster user in follow list algo 2023-04-02 11:42:11 -04:00
Vitor Pamplona
9afc94c050 Display relay info in spam message logs 2023-04-02 11:32:38 -04:00
toadlyBroodle
9d9283e05f clean up imports,
fix translatable typo
2023-04-02 13:58:07 +09:00
toadlyBroodle
5692625922 show amount for atomic polls before zapping 2023-04-02 13:46:24 +09:00
toadlyBroodle
7e07573fa5 fix voting on closed polls bug 2023-04-01 10:23:45 +09:00
toadlyBroodle
1f08f33600 merge with recent changes to main 2023-04-01 09:54:13 +09:00
Vitor Pamplona
1ee3dc0ff3 v0.31.3 2023-03-31 18:36:58 -04:00
Vitor Pamplona
3626f24b5f Adds citations to the notification feed 2023-03-31 18:36:05 -04:00
Vitor Pamplona
4bced4dbea v0.31.2 2023-03-31 17:00:17 -04:00
Vitor Pamplona
737244fb60 Fixes "author_is_elon" bug 2023-03-31 16:59:23 -04:00
Vitor Pamplona
22adbddd88 v0.31.1 2023-03-31 15:41:07 -04:00
Vitor Pamplona
6c5fecc4d2 Loads the replies of the selected note. 2023-03-31 15:30:57 -04:00
Vitor Pamplona
97017e2bd5 v0.31.0 2023-03-31 14:12:59 -04:00
Vitor Pamplona
3145e381a0 Refactoring EOSEs to consider account switching in the app. 2023-03-31 13:50:24 -04:00
Vitor Pamplona
bd62736002 Uses per subscription EOSE to avoid downloading everything again when Resuming the app. 2023-03-31 12:26:13 -04:00
Vitor Pamplona
6884d5e8dd Fixes thread loading issue where the clicked note's children is not loaded. 2023-03-31 10:53:34 -04:00
Vitor Pamplona
27235b0f77 Changes the download folder to remove Amethyst's version. 2023-03-31 10:06:30 -04:00
toadlyBroodle
24dc4ba7a5 remove hacky fun for getting poll_option from ZapEvent 2023-03-31 13:33:42 +09:00
toadlyBroodle
a01a28de0e merge with recent changes to main 2023-03-31 10:50:37 +09:00
Vitor Pamplona
9fa3b1db34 Keeping most items visible when grouping notification cards. 2023-03-30 19:47:48 -04:00
Vitor Pamplona
3193a5e0ef Updating search as new notes come in. 2023-03-30 18:53:20 -04:00
Vitor Pamplona
3137750a58 Centers the loading icon for images. 2023-03-30 18:35:30 -04:00
Vitor Pamplona
20068a40d4 Activates more relays for chats. 2023-03-30 18:23:39 -04:00
Vitor Pamplona
ef5980e90b BugFix: Some events not loading. 2023-03-30 18:16:14 -04:00
Vitor Pamplona
04a3fe2f4c Groups Notifications on Double click in the navigation row. 2023-03-30 17:52:35 -04:00
Vitor Pamplona
33467c62fa Adds PoW Display to the MasterNote of a Thread 2023-03-30 17:46:01 -04:00
Vitor Pamplona
a3e94dbaef Dependency updates 2023-03-30 16:53:13 -04:00
Vitor Pamplona
b4e39f2b73 Full Wallet Connect onboarding with Alby. 2023-03-30 16:10:13 -04:00
Vitor Pamplona
b4e409db4d Saves the position of the Notification feed 2023-03-30 13:58:28 -04:00
Vitor Pamplona
eff07bfb1e Breaks notification cards in chunks of 50 notifications each. 2023-03-30 11:02:39 -04:00
Vitor Pamplona
7f305f498b Only display PoW status with 8 bits of zeros - 0x00.. 2023-03-30 09:10:27 -04:00
Vitor Pamplona
bfb7de896c Merge pull request #325 from ZsZolee/main
Custom Zaps Hungarian Translation
2023-03-30 08:41:06 -04:00
Zoltan
2c8c4bede7 Custom Zaps Hungarian Translation 2023-03-30 10:36:40 +02:00
Vitor Pamplona
b079b3ef79 0.30.2 2023-03-29 18:15:44 -04:00
Vitor Pamplona
a8c8ee1539 BugFix: Stick reply setup from Gigi. 2023-03-29 18:06:41 -04:00
Vitor Pamplona
8f14ccda86 BugFix Sticky notifications on account change 2023-03-29 18:06:19 -04:00
Vitor Pamplona
a66f001033 Adds Image Upload on Profile Edit pages. 2023-03-29 17:22:21 -04:00
Vitor Pamplona
ca79ceb1b9 Adjusts custom zap messages with double click. 2023-03-29 17:00:01 -04:00
Vitor Pamplona
0abac47ff2 More forgiving Wallet Connect interface (nsec or hexes are accepted, relay url is trimmed and cleaned up, if relay doesn't have a wss://, adds one) 2023-03-29 16:21:11 -04:00
Vitor Pamplona
5038588d70 Merge pull request #319 from believethehype/main
Custom Messages for Zaps
2023-03-29 16:05:26 -04:00
Vitor Pamplona
19dab9ef4d Fixes hungarian xml 2023-03-29 15:57:03 -04:00
Vitor Pamplona
374fe85ce9 Merge remote-tracking branch 'origin/HEAD' 2023-03-29 15:52:42 -04:00
Vitor Pamplona
c1120aede3 Merge pull request #324 from believethehype/wallet-connect
Making it clearer in which format the relay should be entered for wallet-connect
2023-03-29 15:52:08 -04:00
Vitor Pamplona
e2f93ec17e Merge pull request #323 from ZsZolee/main
Adding additional Hungarian Translation
2023-03-29 15:51:42 -04:00
Vitor Pamplona
78adb757da Merge pull request #322 from believethehype/profile_longnames
Fixed long usernames in profile
2023-03-29 15:51:21 -04:00
Vitor Pamplona
9a6f88b81b Turning LocalCache Listeners into an Additive Feed type. 2023-03-29 15:14:52 -04:00
Believethehype
3cd6bab7e9 Update UpdateZapAmountDialog.kt 2023-03-29 18:26:44 +02:00
believethehype
1918fe24fc Merge branch 'vitorpamplona:main' into main 2023-03-29 18:24:10 +02:00
Zoltan
5262368b61 Merge pull request #1 from ZsZolee/HUN-add
Adding additional Hungarian Translation
2023-03-29 10:58:42 +02:00
Zoltan
d6a8b70852 Adding additional Hungarian Translation 2023-03-29 10:57:23 +02:00
Believethehype
d61844436d Update ProfileScreen.kt 2023-03-29 10:52:31 +02:00
toadlyBroodle
32067fe800 merge branch polls into main 2023-03-29 16:00:15 +09:00
Vitor Pamplona
e41da35a02 Adds image url when previews can't be loaded. 2023-03-28 18:13:00 -04:00
Vitor Pamplona
b865d15adf Increases speed of the bottom navigation by avoiding recreating ViewModels 2023-03-28 17:11:38 -04:00
Believethehype
42e0cda2e2 don't reset dialog on close 2023-03-28 22:24:00 +02:00
Believethehype
ad70cfb8d7 updated ZapCustomDialog 2023-03-28 22:22:28 +02:00
believethehype
caaf554371 Merge branch 'vitorpamplona:main' into main 2023-03-28 21:15:22 +02:00
Vitor Pamplona
73cabc1b25 Fixing the position of the text in short messages with expandable content. 2023-03-28 11:34:20 -04:00
Vitor Pamplona
9590eecacd Turning Relay icon FlowRow into a LazyVerticalGrid to see if improves rendering speed. 2023-03-28 10:55:30 -04:00
Vitor Pamplona
8eb3a47844 Removing unnecessary logs from the Relay class 2023-03-28 09:01:47 -04:00
Vitor Pamplona
9ae860a66a Moving byte count to count all messages from relays, including errors. 2023-03-28 09:01:26 -04:00
Vitor Pamplona
ceab807f98 BugFix for cutting links in half when they are the last word in a post. 2023-03-28 08:59:08 -04:00
Vitor Pamplona
6b61132f15 Makes the Scan QR Button available in smaller screens. 2023-03-28 08:47:39 -04:00
Vitor Pamplona
cdcf6f9b0a Adds PoW Display to notes 2023-03-28 08:46:07 -04:00
toadlyBroodle
573cfa8b75 add expandable FAB compose menu,
increase bolt size of poll icon
2023-03-28 14:09:07 +09:00
Vitor Pamplona
e1f2313054 v0.30.1 2023-03-27 10:10:04 -04:00
Vitor Pamplona
23a99f3a8c Merge remote-tracking branch 'origin/HEAD' 2023-03-27 09:59:04 -04:00
Vitor Pamplona
53393a350c Fixes Possible crash when typing invalid keys 2023-03-27 09:58:45 -04:00
Vitor Pamplona
56e83d3fe3 Adds special private key for Wallet Connect 2023-03-27 09:54:01 -04:00
believethehype
8aa545f920 Merge branch 'vitorpamplona:main' into main 2023-03-26 23:58:40 +02:00
Believethehype
5f459ac924 Custom Messages for Zaps
- Includes adding a custom message to sats in Backend Code

- Added a simple window on double tap on the bolt symbol for custom amount + message

- ! The popup might need some beautification

- ! Messages are not shown yet in Amethyst

- ! Window could provide options for non-zaps and private zaps in the future
2023-03-26 23:57:25 +02:00
Vitor Pamplona
5ce35363d9 Merge pull request #318 from believethehype/main
Show "follow back" on a user's profile when this user already is a follower
2023-03-26 17:24:44 -04:00
Believethehype
913fa4ba5b Show follow back on profile when profile already follows user 2023-03-26 21:24:13 +02:00
Vitor Pamplona
7a50f97773 0.30.0 2023-03-26 11:55:23 -04:00
Vitor Pamplona
0bd5ad9496 removing align to end of the Auto-translated message. 2023-03-26 11:54:56 -04:00
Vitor Pamplona
2c4be592ad Correctly counts pruned contact lists. 2023-03-26 11:42:30 -04:00
Vitor Pamplona
2183639719 Merge pull request #316 from miseelu/main
Updated Chinese (Traditional) Translation
2023-03-26 11:28:48 -04:00
Vitor Pamplona
0602150cd8 Only runs scroll to top once. 2023-03-26 11:10:26 -04:00
Vitor Pamplona
c7327c8b91 Slimmer look to the Zap button 2023-03-26 10:44:16 -04:00
Vitor Pamplona
e5e47795c1 It should not clear the navigation stack when entering threads. 2023-03-26 10:34:28 -04:00
Vitor Pamplona
0980208cce Merge remote-tracking branch 'origin/better_use_of_eose_at_memory_cost' 2023-03-26 10:11:18 -04:00
Vitor Pamplona
93d6d2ed3e - Preloads URL Previews to avoid jjittering when loading previews.
- Remembers video playback functions to avoid jittering
- Refactors Invalidate Calls into an object.
2023-03-26 10:02:38 -04:00
toadlyBroodle
b65139f520 only allow one vote per option on atomic (main==max) polls
fix buggy click event on disabled zap button
2023-03-26 14:34:32 +09:00
toadlyBroodle
62ff9ac94b color option progress bar green when consensus achieved,
add padding to option text
2023-03-26 13:52:27 +09:00
toadlyBroodle
584c2860e4 move option tally logic to PollNoteViewModel 2023-03-26 09:51:00 +09:00
Vitor Pamplona
4a77d8b134 Videos now go full screen. 2023-03-25 16:51:37 -04:00
Vitor Pamplona
7e89822cbf BugFix for nostr: in content. 2023-03-25 16:16:41 -04:00
Milu Lu
8231e03a94 Updated Chinese(Simplified) Translation 2023-03-26 01:53:10 +08:00
Milu Lu
26e47d7a10 Updated Chinese (Traditional) Translation 2023-03-26 01:52:31 +08:00
Milu Lu
d02ee4dc53 Merge remote-tracking branch 'origin/main' 2023-03-26 01:48:17 +08:00
Vitor Pamplona
1f4f5e3094 Reduces the amount of download from relays, but at increased memory costs. 2023-03-25 12:08:51 -04:00
Vitor Pamplona
fb14c4e872 Merge pull request #315 from miseelu/main
Do not show notes with the creation time exceeding the current time
2023-03-25 12:07:47 -04:00
Milu Lu
ec503f4963 Update translation menu right alignment 2023-03-25 20:13:33 +08:00
Milu Lu
a0869349bd Updated Chinese (Traditional) Translation 2023-03-25 19:52:38 +08:00
Milu Lu
0f71c01632 Updated Chinese(Simplified) Translation 2023-03-25 19:51:51 +08:00
Milu Lu
c2723fe9dd Updated Chinese(Simplified) Translation 2023-03-25 19:44:31 +08:00
Milu Lu
3936edb3b7 Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. 2023-03-25 18:04:46 +08:00
toadlyBroodle
12a1c3fe6d add option tally percentage bars,
create new PollNoteViewModel for each PollNote,
fix isVoteAmountAtomic to directly launch wallet
2023-03-25 15:46:54 +09:00
toadlyBroodle
5b1e7c3451 don't pass in context to voteAmountPlaceHolderText() 2023-03-25 09:18:13 +09:00
toadlyBroodle
9b9cc092dc move PollNote logic to PollNoteViewModel,
fix PollNoteEvent tag getter funs for the final fucking time (hopefully)!,
only show option amounts if user has zapped poll note,
make poll options wider
2023-03-25 08:59:11 +09:00
Vitor Pamplona
5cc0bf54be v0.29.2 2023-03-24 17:25:16 -04:00
Vitor Pamplona
27e053dcd2 Merge remote-tracking branch 'origin/HEAD' 2023-03-24 17:04:50 -04:00
Vitor Pamplona
aaa0678c18 Merge pull request #314 from believethehype/main
#SkullofSatoshi
2023-03-24 17:04:28 -04:00
Vitor Pamplona
cd03ed354f Adjusts zap animation size 2023-03-24 17:03:58 -04:00
Believethehype
10f3551c4f Update HashtagIcon.kt 2023-03-24 21:51:54 +01:00
Believethehype
6bb1f260ea Update HashtagIcon.kt 2023-03-24 21:49:16 +01:00
Believethehype
d889c675a0 Update HashtagIcon.kt 2023-03-24 21:47:35 +01:00
Believethehype
757f6982bd Merge branch 'main' of https://github.com/believethehype/amethyst 2023-03-24 21:35:11 +01:00
Believethehype
d88581ac88 #SkullOfSatoshi 2023-03-24 21:34:29 +01:00
Vitor Pamplona
2b60c24c86 0.29.1 2023-03-24 13:27:29 -04:00
Vitor Pamplona
99f84e891e Merge remote-tracking branch 'origin/HEAD' 2023-03-24 13:19:51 -04:00
Vitor Pamplona
efa0672241 Merge pull request #312 from greenart7c3/main
add missing pt-br translations
2023-03-24 13:17:35 -04:00
Vitor Pamplona
987a3026ac Support for Translateable RichText on About Me section of the Profile 2023-03-24 13:15:23 -04:00
Vitor Pamplona
bbf33df04c Adds an Image Carousel when there are more than 1 image in the post. 2023-03-24 12:33:16 -04:00
Vitor Pamplona
8b81e2e279 Requesting reposts from UserProfile 2023-03-24 10:56:41 -04:00
Vitor Pamplona
3d8870c342 Simplifying the filter for home 2023-03-24 10:56:23 -04:00
Vitor Pamplona
1a923cefaf Removes the tag from the autotranslation index to avoid translating the name itself. 2023-03-24 10:03:56 -04:00
Vitor Pamplona
eed62c36b9 Creates a Loading screen to get events before redirecting them to the appropriate screen (DMs, Channels, Threads) 2023-03-24 09:58:28 -04:00
toadlyBroodle
712c8ab2dd fix isPollOptionZappedBy fun,
option zap widgets layout improvements
2023-03-24 20:26:58 +09:00
greenart7c3
839a691a67 add missing pt-br translations 2023-03-24 08:13:30 -03:00
toadlyBroodle
dbf0256b1c add zap vote amount dialog,
show vote option tallies,
format zapped vote button icons
2023-03-24 18:10:02 +09:00
Vitor Pamplona
e910260375 Merge remote-tracking branch 'origin/HEAD' 2023-03-23 17:58:16 -04:00
Vitor Pamplona
7c9510a6c9 Merge pull request #310 from lgleasain/main
adding in support for clicking on lightning withdrawal links intenting out to a wallet
2023-03-23 17:53:24 -04:00
Vitor Pamplona
3030d474e2 Migrates per-relay stats from event counter to bytes transmitted. 2023-03-23 17:52:26 -04:00
Vitor Pamplona
d9c0b8cd20 Fixes mark for users that pledged sats to a bounty 2023-03-23 17:20:33 -04:00
Lance Gleason
51fbf1b1a5 adding in support for clicking on lightning withdrawal links and having it go to a wallet 2023-03-23 14:52:15 -06:00
Vitor Pamplona
ba33b5e1c6 removes prefixes of unsecured websocket addresses 2023-03-23 16:30:52 -04:00
Vitor Pamplona
81d9037807 Simplifying Reply unwrap for messages made by the logged in Person 2023-03-23 15:47:20 -04:00
Vitor Pamplona
79c894dcfd Moving Markdown default styling to a singleton 2023-03-23 15:21:44 -04:00
Vitor Pamplona
e4c98cf39f Fixing spaces after url 2023-03-23 15:21:25 -04:00
Vitor Pamplona
ed1e080fbe Support for r tags 2023-03-23 15:20:35 -04:00
Vitor Pamplona
afef10189b v0.29.0 2023-03-23 12:32:14 -04:00
Vitor Pamplona
1a66319a6d Support to pledge additional bounty amounts. 2023-03-23 12:24:46 -04:00
Vitor Pamplona
7a1470ef7b BugFix: Don't add e tags with addresses. 2023-03-23 12:24:11 -04:00
Vitor Pamplona
be09577db1 Adds extra space before uncited hashtags. 2023-03-23 11:07:29 -04:00
Vitor Pamplona
f91a0d4cd0 Fixes hashtag links 2023-03-23 11:07:02 -04:00
Vitor Pamplona
4589e6f329 Adding Bounty information to Note 2023-03-23 10:49:01 -04:00
Vitor Pamplona
7375c9e9e3 Adds border for Markdown texts 2023-03-23 10:46:59 -04:00
Vitor Pamplona
ccd71a8023 Adds default banner image to LongFormPosts if none is found 2023-03-23 09:04:44 -04:00
Vitor Pamplona
3a76d176c5 Adds user's picture and background to LongForm posts without a default image 2023-03-23 08:58:18 -04:00
Vitor Pamplona
6626be8e1c Displays hashtags that are not in the text at the bottom of each note 2023-03-23 08:57:28 -04:00
toadlyBroodle
171a7841b3 add zap buttons to PollNote options,
add poll_option tag and handlers to LnZapEvents and zap funs,
2023-03-23 19:11:07 +09:00
toadlyBroodle
611dcc0189 implement new NIP69 poll_option tags format List<List<String>>,
fix PollNoteEvent field getter return types
2023-03-23 16:18:16 +09:00
Vitor Pamplona
dd9ea5f27e Support for video uploads. 2023-03-22 16:50:18 -04:00
Vitor Pamplona
cb20e14822 Sending Addressable notes to Habla.news: Fixes: https://github.com/vitorpamplona/amethyst/issues/295 2023-03-22 15:47:01 -04:00
Vitor Pamplona
b9aa099a1e Adding space on inner notes 2023-03-22 15:03:14 -04:00
Vitor Pamplona
1e8bc10ad1 Showing progress bars when Zaps are requested 2023-03-22 14:24:33 -04:00
Vitor Pamplona
ba40b1815f Checks if the requested amount to matches the received LN Invoice's amount 2023-03-22 14:23:42 -04:00
Vitor Pamplona
cf2c56e088 Adds instructions of use for the wallet connect 2023-03-22 11:56:12 -04:00
Vitor Pamplona
896758121b Blocks translations of @npub/@note tags in the text. 2023-03-22 11:24:58 -04:00
Vitor Pamplona
f0f9726ede Support for Wallet Connect Api 2023-03-22 09:45:21 -04:00
Vitor Pamplona
ddb3990c11 Fully hiding chat messages from hidden users. 2023-03-21 14:15:58 -04:00
Vitor Pamplona
8a72e3b7ac Reducing the amount of preloaded messages in chat. 2023-03-21 14:15:26 -04:00
Vitor Pamplona
aae1a65cba Refactoring: notNull filter is not needed. 2023-03-21 14:13:32 -04:00
Vitor Pamplona
fb017c3903 Merge pull request #299 from believethehype/main
Minor Cosmetics for HashTagIcons
2023-03-21 08:29:23 -04:00
toadlyBroodle
ef31f56eab add PollNote type to ThreadFeedView,
simplify PollNote() arguments,
use parent's backgroundColor,
breakout PollNote modifier
2023-03-21 20:52:30 +09:00
toadlyBroodle
771cdd6ebe add PollNote to ui.note,
simplify code for displaying PollNoteEvents with replyTos or mentions,
fix pollOptions (and other poll fields) get from tags functions
2023-03-21 20:05:53 +09:00
Believethehype
0c1b9195d7 higher resolution icons, fixed descriptions, positioning 2023-03-21 10:27:38 +01:00
Believethehype
1fe58810a0 2 additional popular community hashtagicons
plebchain / coffeechain
2023-03-21 08:46:19 +01:00
Believethehype
3cde9bcdd5 Update HashtagIcon.kt 2023-03-21 08:21:56 +01:00
Believethehype
0c3b73f4d3 Minor Cosmetics for HashTagIcons
- Made Icons a little bit larger again, as it was before last (scaling fix) update.

- Added some more popular variations of currently supported hts.
2023-03-21 08:19:18 +01:00
Vitor Pamplona
25883d00ce v0.28.1 2023-03-20 20:57:12 -04:00
Vitor Pamplona
d04bbfc640 Merge pull request #297 from believethehype/main
Better scaling for HashtagIcons for different System font sizes
2023-03-20 20:50:11 -04:00
Vitor Pamplona
7da837f49e Always hide the note if the user has selected to hide user. 2023-03-20 20:46:25 -04:00
Vitor Pamplona
9709ffe388 Removes the possibility of duplicated event in Bookmarks 2023-03-20 20:45:36 -04:00
Believethehype
f7cc656288 Update HashtagIcon.kt 2023-03-21 01:08:15 +01:00
Believethehype
3ba979e513 Update HashtagIcon.kt 2023-03-21 00:55:27 +01:00
Vitor Pamplona
eadb4a108c Fixing EO Translations 2023-03-20 18:50:25 -04:00
Vitor Pamplona
8b44816520 v0.28.0 2023-03-20 18:26:03 -04:00
Vitor Pamplona
2fb673acf0 Bookmarks 2023-03-20 18:16:07 -04:00
Believethehype
8401f09475 Update HashtagIcon.kt 2023-03-20 20:39:32 +01:00
Believethehype
c4400ded28 Better scaling for HashtagIcons for different System font sizes 2023-03-20 20:32:56 +01:00
Vitor Pamplona
fc37789727 Adds a filter for messages by the author in public chats from all relays. 2023-03-20 12:10:09 -04:00
Vitor Pamplona
abb27ac811 Refactors ChatroomList datasource startup to be more explicit 2023-03-20 09:44:11 -04:00
Vitor Pamplona
fbf5fb4aab Merge pull request #290 from believethehype/main
simplified HashTagIcon code, new suggestions for #lightning and #zap
2023-03-20 08:59:41 -04:00
Vitor Pamplona
3887a6ae44 Merge pull request #291 from Radiokot/fix/actualize_ru_uk_23_03_18
Actualize Russian and Ukrainian translations
2023-03-20 08:45:49 -04:00
Vitor Pamplona
cfb342cd74 Merge pull request #294 from chiajlingvoj/main
create strings.xml for Eo translation
2023-03-20 08:45:01 -04:00
Vitor Pamplona
2d693145d9 Follows UserAgent version syntax: User-Agent: <product> / <product-version> <comment> 2023-03-20 08:41:03 -04:00
Vitor Pamplona
d62a2480f4 Merge pull request #296 from hsoc/useragent-ver
User-Agent fix
2023-03-20 08:36:13 -04:00
John Economou
b30fb1d9c7 User-Agent fix
include client version in user-agent
2023-03-20 13:43:25 +02:00
toadlyBroodle
d1f61c0bba add PollNoteEvent type to all relevant filters, dataSources, etc. 2023-03-20 17:38:14 +09:00
toadlyBroodle
5d04f3ea99 enable send poll event,
add PollNoteEvent.kind to DataSources,
add consume(PollNoteEvent) to LocalCache,
2023-03-20 15:36:12 +09:00
toadlyBroodle
bd37e4a9df fix add new poll option button onclick,
prefix all poll composables with 'New',
move all poll composables to actions folder,
move jsonToPollOptions parser to PollNoteEvent
2023-03-20 10:20:39 +09:00
toadlyBroodle
3cdfdfa8e8 rewrite pollOptions json parser 2023-03-20 09:52:23 +09:00
chiajlingvoj
fb7c7a723b create strings.xml for Eo translation 2023-03-19 09:50:11 -04:00
toadlyBroodle
8171c1ee5e change pollOptions to type Map<Int, String>,
remove always true condition from Account.isAcceptable()
2023-03-19 22:15:00 +09:00
toadlyBroodle
0366f6235c move valid input flags to pollViewModel,
disable sendPoll button unless all fields valid
2023-03-19 14:08:37 +09:00
toadlyBroodle
7890ac9db5 create pollSend funs, 2023-03-19 12:43:15 +09:00
Oleg Koretsky
fd8517e047 Actualize Russian and Ukrainian translations 2023-03-18 11:25:03 +02:00
toadlyBroodle
7a53708ccc add fields to pollNoteEvent,
add clearState fun to pollViewModel,
add refined PollNoteEvent to Event,
2023-03-18 15:38:58 +09:00
Believethehype
ab0361bf27 Update zap.xml 2023-03-18 03:39:15 +01:00
Believethehype
eb40540f0d Update zap.xml 2023-03-18 03:32:26 +01:00
Believethehype
c1f8116a5a simplified HashTagIcon code, new suggestions for #lightning and #zap 2023-03-18 03:12:24 +01:00
Vitor Pamplona
40aadf77f7 Fixing the correct tagging with tags are placed together with additional characters. 2023-03-17 19:25:51 -04:00
Vitor Pamplona
adafb6b183 No need for spaces after npubs anymore 2023-03-17 19:25:11 -04:00
Vitor Pamplona
d38a7169b8 Fixing spaces after urls 2023-03-17 19:09:43 -04:00
Vitor Pamplona
53705f214a Adding relay icon from https://github.com/vitorpamplona/amethyst/issues/188 2023-03-17 19:09:23 -04:00
Vitor Pamplona
155bb80118 Merge pull request #288 from believethehype/main
Fix for hashtag icon resolutions
2023-03-17 18:49:02 -04:00
Believethehype
de77f2e18b typo 2023-03-17 23:39:23 +01:00
Vitor Pamplona
b2f05b4247 Fixing mutable object to recalculate zap amount. 2023-03-17 18:38:52 -04:00
Believethehype
88292aa179 updated icons 2023-03-17 23:34:59 +01:00
Vitor Pamplona
79ba910f52 Not showing reply information in inner short posts. 2023-03-17 18:01:57 -04:00
Vitor Pamplona
30402b26e8 Removing double space in " and " 2023-03-17 18:01:32 -04:00
Vitor Pamplona
cb6401c7ab Don't preview urls without schema. Show inline link instead. 2023-03-17 18:01:08 -04:00
believethehype
435e99e81a Merge branch 'vitorpamplona:main' into main 2023-03-17 22:59:05 +01:00
Believethehype
4b7167d5ef additional zap hts 2023-03-17 22:58:17 +01:00
Believethehype
9887ec2d38 added additional resolutions for hashtagicons, added #zap #lightning #amethyst 2023-03-17 22:53:39 +01:00
Vitor Pamplona
edb61e2423 No need for NIP05 in Quoted posts 2023-03-17 17:08:55 -04:00
Vitor Pamplona
8b134892ce Marks PlayStore as default for now 2023-03-17 16:54:57 -04:00
Vitor Pamplona
21cb82a05d v0.027.2 2023-03-17 14:40:35 -04:00
Vitor Pamplona
412c5c557a Adding the suffix after the hashtag icon 2023-03-17 14:06:00 -04:00
Vitor Pamplona
81e31319c7 Making tags case insensitive 2023-03-17 10:22:58 -04:00
Vitor Pamplona
97ec28d2c8 Additional suffix support for hashtags. 2023-03-17 10:09:08 -04:00
Vitor Pamplona
c6906c42fd Mocking Android.Log for test classes 2023-03-17 09:47:47 -04:00
Vitor Pamplona
7921db3ba9 Scaling hashtag images 2023-03-17 09:43:09 -04:00
Vitor Pamplona
b12413cbff Removing comments from NIP19 Parser 2023-03-17 09:42:54 -04:00
Vitor Pamplona
07cd1738f1 Merge remote-tracking branch 'origin/HEAD' 2023-03-17 09:30:33 -04:00
Vitor Pamplona
b8aa7e64b3 Merge pull request #284 from believethehype/main
Added Special Bitcoin Hashtag with Icon
2023-03-17 09:30:06 -04:00
Vitor Pamplona
055ed62cb7 Merge remote-tracking branch 'origin/HEAD' 2023-03-17 09:29:26 -04:00
Vitor Pamplona
578c70d272 Correctly parsing NIP19s in Text 2023-03-17 09:28:48 -04:00
Vitor Pamplona
d43f0435d0 Merge pull request #287 from ZsZolee/main
Minor changes
2023-03-17 09:23:12 -04:00
Believethehype
52978fa237 Hashtags in front of a sentence ending should nod work, too. 2023-03-17 13:17:19 +01:00
Believethehype
f266a9e964 Special Hashtag for #Nostr 2023-03-17 12:38:47 +01:00
Zoltan
01cb27e6f5 Minor changes
Minor changes in the Hungarian translation
2023-03-17 10:12:50 +01:00
toadlyBroodle
551ed64e98 add PollNoteEvent type 2023-03-17 16:57:17 +09:00
toadlyBroodle
f0b4660719 validate user input for all poll fields,
add user's pubkey to recipients field,
add poll field saveable texts to pollViewModel,
2023-03-17 16:48:26 +09:00
Believethehype
38730ea534 fixed some linting issues 2023-03-17 08:03:47 +01:00
believethehype
a955f55fc6 Merge branch 'vitorpamplona:main' into main 2023-03-17 07:24:29 +01:00
Believethehype
a42cdaaf91 added special Bitcoin Hashtag with Icon
(S/O to tata)
2023-03-16 23:44:25 +01:00
Vitor Pamplona
0633707ec6 Disabling online search after leaving the Search screen 2023-03-16 16:33:17 -04:00
Vitor Pamplona
8768daa06f Fixing Background color of the badge presentation 2023-03-16 16:32:42 -04:00
Vitor Pamplona
fb0a7b0ae4 Fixing missing messages on chat 2023-03-16 16:31:42 -04:00
Vitor Pamplona
f8b6b8fb45 v0.27.1 2023-03-16 14:24:58 -04:00
Vitor Pamplona
888b6daa2a Support for Following Hashtags 2023-03-16 14:20:30 -04:00
toadlyBroodle
29c4c42547 delete pollOption values on cancel(),
move recipients field to optional section,
make recipients, options textFields fillMaxWidth
2023-03-16 19:25:54 +09:00
toadlyBroodle
107e09aefb move pollOptions list to pollViewModel,
add/delete poll options functionality,
don't allow deleting first 2 poll options
2023-03-16 19:06:00 +09:00
Vitor Pamplona
811c373e4c v0.27.0 2023-03-15 17:07:06 -04:00
Vitor Pamplona
1812ec296e Hashtags 2023-03-15 17:02:49 -04:00
Vitor Pamplona
59cdd81aec Shows LongForm Texts on Global 2023-03-15 17:02:13 -04:00
Vitor Pamplona
9297afc02a Deleting contact lists from pausing the app 2023-03-15 15:13:23 -04:00
Vitor Pamplona
a8d8be3307 Removing unused mentions in LocalCache 2023-03-15 14:35:10 -04:00
Vitor Pamplona
53ec6c817c Fixes the deletion of private messages and messages in tags 2023-03-15 14:28:03 -04:00
Vitor Pamplona
b7c4ec8358 Shows url host in the link preview 2023-03-15 14:06:26 -04:00
Vitor Pamplona
3426ff307c Makes sure onClick and onLongClick are null when their counterparts are as well. 2023-03-15 13:58:09 -04:00
Vitor Pamplona
cddb713b2f Adds the following icon to the chat header 2023-03-15 13:52:26 -04:00
Vitor Pamplona
1871fe6177 Locally cache following Users in the Home & Conversations filter 2023-03-15 13:40:26 -04:00
Vitor Pamplona
83a8558f23 Caching followingChannels and followingUsers to speed Global filter up 2023-03-15 13:39:47 -04:00
Vitor Pamplona
dbc155454f Checks for hex keys in the tag list before showing follows 2023-03-15 13:39:06 -04:00
Vitor Pamplona
80e251acfb Avoids creating User for all awardees of a badge. 2023-03-15 13:22:11 -04:00
Vitor Pamplona
a597bd7882 Checks the mentions for a valid Hex before creating users in the cache. 2023-03-15 13:14:10 -04:00
Vitor Pamplona
a53bb53c12 Avoids breaking urls and invoices when cutting an expandable content 2023-03-15 13:13:41 -04:00
Vitor Pamplona
4fa7aa54a8 Refactoring Note's hasReports function 2023-03-15 13:12:55 -04:00
Vitor Pamplona
a9490e1299 Refactoring calls to check if a user is the signed in user. 2023-03-15 13:12:21 -04:00
Vitor Pamplona
2bf36f1cfe Implements a faster AuthorGallery for notifications 2023-03-15 11:39:30 -04:00
Vitor Pamplona
c781d5eff7 Forces image size in profile pictures to reduce calculations before resizing the downloaded image. 2023-03-15 11:21:53 -04:00
Vitor Pamplona
e8d84795d5 Reduces badge size in Profiles 2023-03-15 11:21:00 -04:00
Vitor Pamplona
b351a781a9 Removes time measuring functions from RoboHash 2023-03-15 11:20:36 -04:00
Vitor Pamplona
97d9e96e2b Uses cached functions for isFollowing calculations 2023-03-15 11:20:09 -04:00
Vitor Pamplona
625b8c3bce Makes the reporting Menu less starkey in dark theme 2023-03-15 09:11:55 -04:00
Vitor Pamplona
edd4a4cc2c Merge pull request #275 from maxmoney21m/feature/216-merge-reports
Add separate screen for report & block
2023-03-15 08:09:07 -04:00
Vitor Pamplona
b305ccad1d Merge pull request #280 from maxmoney21m/bugfix/gitignore
Remove ignored file
2023-03-15 08:08:39 -04:00
Vitor Pamplona
14ca6f1d08 Merge pull request #281 from maxmoney21m/bugfix/string-parameters
Add parameters for strings with variables
2023-03-15 08:08:05 -04:00
maxmoney21m
ad4e6d6596 Add Block button and dialog 2023-03-15 13:40:59 +08:00
maxmoney21m
e94d2b5340 Update gitignore 2023-03-15 11:29:05 +08:00
maxmoney21m
0241bf0913 Add report & block screen 2023-03-15 11:29:04 +08:00
maxmoney21m
36885eb6dc Add parameters for strings with variables 2023-03-15 11:26:51 +08:00
maxmoney21m
c1917855d0 Remove ignored file 2023-03-15 10:42:53 +08:00
toadlyBroodle
63ad7fd205 add PollClosing component,
rename poll strings/values
2023-03-13 17:33:00 +09:00
toadlyBroodle
c31b99b1dc add consensus threshold component/preview,
change value range placeholder strings
2023-03-13 16:32:38 +09:00
toadlyBroodle
851ec71f4a add PollVoteValueRange component,
add PollOption, PollVoteValueRange component previews,
rename poll_recipients string
2023-03-13 15:46:48 +09:00
toadlyBroodle
e30b4f7c7e add PollRecipientsField,
add buttons for add/delete options,
format poll text fields,
change poll option placeholder string
rename primaryPollDescription string
2023-03-13 14:39:40 +09:00
toadlyBroodle
00f9f7ba52 add PollPrimaryDescription, PollOption objects 2023-03-13 13:15:40 +09:00
toadlyBroodle
564f926213 remove closePollButton 2023-03-11 18:57:02 +09:00
toadlyBroodle
0cf891bb4d pollViewModel -> override postViewModel funs,
remove imageUpload from pollView,
2023-03-11 18:16:47 +09:00
toadlyBroodle
fba873dab9 add floating action buttons column 2023-03-11 16:57:09 +09:00
toadlyBroodle
322853f696 ignore .idea/deploymentTargetDropDown, remove duplicate ignore rules 2023-03-11 15:22:33 +09:00
toadlyBroodle
19c3421e92 ignore .idea/deploymentTargetDropDown 2023-03-11 15:20:25 +09:00
toadlyBroodle
206d6c68bb Add new poll button, view, viewModel, icons, string; replace fab with pollButton; supress missing translations errors when debugging 2023-03-11 15:13:02 +09:00
495 changed files with 66753 additions and 9775 deletions

View File

@@ -14,10 +14,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 11
java-version: 17
- name: Cache gradle
uses: actions/cache@v1

View File

@@ -12,10 +12,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 11
java-version: 17
- name: Cache gradle
uses: actions/cache@v1
@@ -47,7 +47,7 @@ jobs:
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Build APK
run: ./gradlew assembleRelease --stacktrace --no-daemon
run: ./gradlew assembleRelease --stacktrace
- name: Sign APK (Google Play)
uses: r0adkll/sign-android-release@v1

2
.gitignore vendored
View File

@@ -6,6 +6,7 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store
/build
/captures
@@ -68,6 +69,7 @@ render.experimental.xml
.idea/**/caches/
.idea/**/libraries/
.idea/**/shelf/
.idea/**/codeStyles
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/.name

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.10" />
<option name="version" value="1.8.21" />
</component>
</project>

10
.idea/misc.xml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -1,13 +1,48 @@
# Amethyst Privacy Policy & Terms of Use
# Amethyst Privacy Policy and Terms of Use
Effective as of Jan 20, 2023.
## Privacy Policy
The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.
Effective as of Jun 12, 2023
Data from connected accounts is only stored locally on the device when it is required for functionality and performance of Amethyst. This data is strictly confidental and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.
The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.
You cannot use Amethyst to submit Objectionable Content to relays. Objectionable Content includes, but is not limited to: (i) sexually explicit materials; (ii) obscene, defamatory, libelous, slanderous, violent and/or unlawful content or profanity; (iii) content that infringes upon the rights of any third party, including copyright, trademark, privacy, publicity or other personal or proprietary right, or that is deceptive or fraudulent; (iv) content that promotes the use or sale of illegal or regulated substances, tobacco products, ammunition and/or firearms; and (v) illegal content related to gambling.
The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts are only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.
We reserve the right to modify this Privacy Policy at any time. Any modifications to this Privacy Policy will be effective upon our posting the new terms and/or upon implementation of the new changes on the Service (or as otherwise indicated at the time of posting). In all cases, your continued use of the app after the posting of any modified Privacy Policy indicates your acceptance of the terms of the modified Privacy Policy.
Amethyst offers a few options to upload pictures and videos in order to post them online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use.
### Privacy with Relays services
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
The relay can also see which public keys you are using and what information you are requesting from the network. Your public key is tied to your IP address and your relay filters.
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of the content inside Private Zaps and Private DMs.
While the content of direct messages (DMs) is only visible to you, and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link on the DM text. Your uploaded pictures are available to anyone with that direct link.
### Visibility & Permanence of Your Content on Nostr Relays
#### Information Visibility
Content that you share can be shared with other relays by any user of the network.
Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to Nostr users who do not share relays with you.
#### Information Permanence
Information shared on Nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
## Terms of Use
### For versions downloaded from Google's Play Store
You cannot use the Amethyst app for Android to submit Objectionable Content to relays. Objectionable Content includes but is not limited to: (i) sexually explicit materials; (ii) obscene, defamatory, libelous, slanderous, violent and/or unlawful content or profanity; (iii) content that infringes upon the rights of any third party, including copyright, trademark, privacy, publicity or other personal or proprietary rights, or that is deceptive or fraudulent; (iv) content that promotes the use or sale of illegal or regulated substances, tobacco products, ammunition and/or firearms; and (v) illegal content related to gambling.
### For versions downloaded from F-Droid
We do not control the distribution of the application in F-Droid. Legal matters should be resolved between the user and F-Droid.
## Other Notes
We reserve the right to modify this Privacy Policy and Terms of Use at any time. Any modifications to this document will be effective upon our posting of the new terms and/or upon implementation of the new changes on the Service (or as otherwise indicated at the time of posting). In all cases, your continued use of the app after the posting of any modified Privacy Policy and Terms of Use indicates your acceptance of the terms of the modified Privacy Policy and/or Terms of Use.
If you have any questions about Amethyst or this privacy policy, you can send a message to amethyst@vitorpamplona.com

View File

@@ -4,13 +4,27 @@
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
[<img src="./docs/design/obtainium.png"
alt="Get it on Obtaininum"
height="80">](https://github.com/ImranR98/Obtainium)
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
height="80">](https://github.com/vitorpamplona/amethyst/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
# Current Features
- [x] Event Builders / WebSocket Subscriptions (NIP-01, NIP-15)
- [x] Event Builders / WebSocket Subscriptions (NIP-01)
- [x] Home Feed
- [x] Notifications Feed
- [x] Global Feed
- [x] Reactions (likes NIP-25, boost, reply)
- [x] Replies and mentions (NIP-10)
- [x] Reactions (NIP-25)
- [x] Reposts (NIP-18)
- [x] Image/Video/Url/LnInvoice Previews
- [x] View Threads
- [x] Private Messages (NIP-04)
@@ -23,32 +37,64 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [x] Relay Sets (home, dms, public chats, global)
- [x] User/Note Tagging (NIP-08, NIP-10)
- [x] Lightning Tips
- [x] Zaps (NIP-57)
- [x] Zaps (private, public, anon, non-zap) (NIP-57)
- [x] URI Support (NIP-21)
- [x] Event Deletion (NIP-09: like, boost, text notes and reports)
- [x] Identity Verification (NIP-05)
- [x] Created_at Limits (NIP-22)
- [x] Event Deletion (NIP-09)
- [x] Nostr Address (NIP-05)
- [x] Long-form Content (NIP-23)
- [x] Parameterized Replaceable Events (NIP-33)
- [x] Online Relay Search (NIP-50)
- [x] Internationalization
- [x] Badges (NIP-58)
- [ ] Local Database
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [x] Hashtag Following and Custom Hashtags
- [x] Polls (NIP-69)
- [x] Verifiable static content in URLs (NIP-94)
- [x] Login with QR
- [x] Wallet Connect API (NIP-47)
- [x] Accessible uploads
- [x] Bounty support (nostrbounties.com)
- [x] De-googled F-Droid flavor
- [x] External Identity Support (NIP-39)
- [x] Multiple Accounts
- [x] Markdown Support
- [x] Relay Authentication (NIP-42)
- [x] Content stored in relays themselves (NIP-95)
- [x] Custom Emoji (NIP-30)
- [x] Zap Forwarding (NIP-57)
- [x] Text Note References (NIP-27)
- [x] Audio Tracks (zapstr.live) (Kind:31337)
- [x] Push Notifications (Zaps and Messages)
- [x] Generic Tags (NIP-12)
- [x] Sensitive Content (NIP-36)
- [x] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [x] Recommended Application Handlers (NIP-89)
- [x] Events with a Subject (NIP-14)
- [x] Generic Reposts (kind:16)
- [x] Live Activities & Live Chats (NIP-53)
- [x] Relay Pages (NIP-11)
- [x] HTTP Auth (NIP-98)
- [x] Zapraiser (NIP-TBD)
- [x] Moderated Communities (NIP-172)
- [x] Emoji Packs (Kind:30030)
- [x] Personal Emoji Lists (Kind:10030)
- [x] Classifieds (Kind:30403)
- [ ] Marketplace (NIP-15)
- [ ] Image/Video Capture in the app
- [ ] Local Database
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
- [ ] Sensitive Content (NIP-36)
- [ ] Relay Pages (NIP-11)
- [ ] Relay Authentication (NIP-42)
- [ ] Generic Tags (NIP-12)
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
- [ ] Events with a Subject (NIP-14)
- [ ] Workspaces
- [ ] Expiration Support (NIP-40)
- [ ] Infinity Scroll
- [ ] Relay List Metadata (NIP-65)
- [ ] Signing Requests (NIP-46)
- [ ] Delegated Event Signing (NIP-26)
- [ ] Account Creation / Backup Guidance (NIP-06)
- [ ] Account Creation / Backup Guidance
- [ ] Mnemonic seed phrase (NIP-06)
- [ ] Message Sent feedback (NIP-20)
- [ ] OpenTimestamps Attestations (NIP-03)
# Development Overview
@@ -74,7 +120,7 @@ Lastly, the user's account information (priv key/pub key) is stored in the Andro
## Setup
Make sure to have the following pre-requisites installed:
1. Java 11
1. Java 17
2. Android Studio
3. Android 8.0+ Phone or Emulation setup
@@ -104,8 +150,15 @@ Build the app:
```
## Installing on device
For the F-Droid build:
```bash
./gradlew installDebug
./gradlew installFdroidDebug
```
For the Play build:
```bash
./gradlew installPlayDebug
```
## How to Deploy
@@ -145,10 +198,22 @@ Information that you share is publicly visible to anyone reading from relays tha
## Information Permanence ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
# Screenshots
| Home Feed | Messages | Live Streams | Notifications |
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
| ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) |
# Contributing
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome.
## Contributors
<a align="center" href="https://github.com/vitorpamplona/amethyst/graphs/contributors">
<img src="https://contrib.rocks/image?repo=vitorpamplona/amethyst" />
</a>
# MIT License
Copyright (c) 2023 Vitor Pamplona

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -2,6 +2,7 @@ plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jlleitschuh.gradle.ktlint' version "11.3.1"
id 'com.google.gms.google-services'
}
android {
@@ -12,17 +13,19 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 98
versionName "0.26.2"
versionCode 261
versionName "0.70.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
resConfigs("ar", "cs", "de", "eo", "es", "fa", "fr", "hu", "night", "nl", "pt-rBR", "ru", "sv-rSE", "ta", "tr", "uk", "zh", "sh-rHK", "zh-rTW", "ja")
}
buildTypes {
release {
// TODO: Make sure all of JSON parsers work when activating these.
//minifyEnabled true
//proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "string", "app_name", "@string/app_name_release"
@@ -38,6 +41,7 @@ android {
productFlavors {
play {
getIsDefault().set(true)
dimension "channel"
}
@@ -56,12 +60,12 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = '17'
}
buildFeatures {
@@ -69,7 +73,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion "1.4.3"
kotlinCompilerExtensionVersion "1.4.7"
}
packagingOptions {
@@ -77,11 +81,14 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
lint {
disable 'MissingTranslation'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.activity:activity-compose:1.7.0-beta01'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
@@ -103,11 +110,13 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
implementation "net.engawapg.lib:zoomable:1.4.3"
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
// Bitcoin secp256k1 bindings to Android
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.7.1'
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1'
// Nostr Base Protocol
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
@@ -120,34 +129,33 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
// Json Serialization TODO: We might need to converge between gson and Jackson (we are usin both)
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// link preview
implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
implementation 'org.jsoup:jsoup:1.16.1'
// Encrypted Key Storage
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
// view videos
implementation 'com.google.android.exoplayer:exoplayer:2.18.4'
implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
// important for proxy / tor
implementation "androidx.media3:media3-datasource-okhttp:${exoplayer_version}"
// Load images from the web.
implementation "io.coil-kt:coil-compose:2.2.2"
implementation "io.coil-kt:coil-compose:$coil_version"
// view gifs
implementation "io.coil-kt:coil-gif:2.2.2"
implementation "io.coil-kt:coil-gif:$coil_version"
// view svgs
implementation("io.coil-kt:coil-svg:2.2.2")
implementation "io.coil-kt:coil-svg:$coil_version"
// create blurhash
implementation group: 'io.trbl', name: 'blurhash', version: '1.0.0'
// Rendering clickable text
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
// Permission to upload pictures:
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
// tabs for user profiles
implementation "com.google.accompanist:accompanist-pager:$accompanist_version" // Pager
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"
// Parses URLs from Text:
implementation "io.github.url-detector:url-detector:0.1.23"
@@ -156,9 +164,20 @@ dependencies {
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
// Markdown
implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
// Markdown (With fix for full-image bleeds)
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:a0954aba63')
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63')
// Language picker and Theme chooser
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.1.0'
implementation 'androidx.media3:media3-ui:1.1.0'
implementation 'androidx.media3:media3-session:1.1.0'
// Local model for language identification
playImplementation 'com.google.mlkit:language-id:17.0.4'
@@ -166,12 +185,30 @@ dependencies {
// Google services model the translate text
playImplementation 'com.google.mlkit:translate:17.0.1'
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.1.1')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
// Charts
implementation "com.patrykandpatrick.vico:core:${vico_version}"
implementation "com.patrykandpatrick.vico:compose:${vico_version}"
implementation "com.patrykandpatrick.vico:views:${vico_version}"
implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}"
// immutable collections to avoid recomposition
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// Video compression lib
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
// Image compression lib
implementation 'id.zelory:compressor:3.0.1'
// Automatic memory leak detection
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation 'junit:junit:4.13.2'
testImplementation "io.mockk:mockk:1.13.4"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation 'io.mockk:mockk:1.13.5'
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"

68
app/google-services.json Normal file
View File

@@ -0,0 +1,68 @@
{
"project_info": {
"project_number": "768341258853",
"project_id": "amethyst-3057a",
"storage_bucket": "amethyst-3057a.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:5d07c35a37b24ff36b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:e0200680f552484d6b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst.debug"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -0,0 +1,89 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CitationTests {
val json = """
{
"content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
"created_at": 1683596206,
"id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
"kind": 1,
"pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
"sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
"tags": [
[
"e",
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"",
"root"
],
[
"e",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
"",
"reply"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"
],
[
"p",
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"
],
[
"p",
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"
],
[
"p",
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"
],
[
"p",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
],
[
"p",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"
],
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://nostr.wine/"
]
}
"""
@Test
fun parseEvent() {
val event = Event.fromJson(json, true) as TextNoteEvent
val expectedCitations = setOf(
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
)
assertEquals(expectedCitations, event.citedUsers())
}
}

View File

@@ -0,0 +1,113 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Base64
@RunWith(AndroidJUnit4::class)
class ImageUploadTesting {
val image = "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw=="
@Test()
fun testImgurUpload() = runBlocking {
val bytes = Base64.getDecoder().decode(image)
val inputStream = bytes.inputStream()
ImageUploader.uploadImage(
inputStream,
bytes.size.toLong(),
"image/gif",
ImgurServer(),
onSuccess = { url, contentType ->
println("Uploaded to $url")
assertNotNull(url)
},
onError = {
println("Failed to Upload")
fail("${it.message}")
}
)
delay(5000)
}
@Test()
fun testNostrBuildUpload() = runBlocking {
val bytes = Base64.getDecoder().decode(image)
val inputStream = bytes.inputStream()
ImageUploader.uploadImage(
inputStream,
bytes.size.toLong(),
"image/gif",
NostrBuildServer(),
onSuccess = { url, contentType ->
println("Uploaded to $url")
assertNotNull(url)
},
onError = {
println("Failed to Upload")
fail("${it.message}")
}
)
delay(1000)
}
@Test()
fun testNostrImgUpload() = runBlocking {
val bytes = Base64.getDecoder().decode(image)
val inputStream = bytes.inputStream()
ImageUploader.uploadImage(
inputStream,
bytes.size.toLong(),
"image/gif",
NostrImgServer(),
onSuccess = { url, contentType ->
println("Uploaded to $url")
assertNotNull(url)
},
onError = {
println("Failed to Upload")
fail("${it.message}")
}
)
delay(1000)
}
@Test()
fun testNostrFilesDevUpload() = runBlocking {
val bytes = Base64.getDecoder().decode(image)
val inputStream = bytes.inputStream()
ImageUploader.uploadImage(
inputStream,
bytes.size.toLong(),
"image/gif",
NostrFilesDevServer(),
onSuccess = { url, contentType ->
println("Uploaded to $url")
assertNotNull(url)
},
onError = {
println("Failed to Upload")
fail("${it.message}")
}
)
delay(5000)
}
}

View File

@@ -0,0 +1,138 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent.Companion.createEncryptionPrivateKey
import fr.acinq.secp256k1.Hex
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PrivateZapTests {
@Test
fun testPollZap() {
val poll = Event.Companion.fromJson(
"""{
"content": "New poll \n\n #zappoll",
"created_at": 1682440713,
"id": "16291ba452bb0786a4bf5c278d38de73c96b58c056ed75c5ea466b0795197288",
"kind": 6969,
"pubkey": "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
"sig": "ac05fa4004c3f7c42913c87b11bf9714bb61a3f0940863a6b9ff0f8105b399add72dbc09bf944c79b9a72ef009ec6905adedbd2c4c8fb3d2f57007bad8fcb279",
"tags": [
[
"poll_option",
"0",
"Test 1"
],
[
"poll_option",
"1",
"Test 2"
],
[
"value_maximum",
"null"
],
[
"value_minimum",
"null"
],
[
"consensus_threshold",
"null"
],
[
"closed_at",
"null"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""",
true
)
val loggedIn = Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")
val privateZapRequest = LnZapRequestEvent.create(
poll,
setOf("wss://relay.damus.io/"),
loggedIn,
0,
"",
LnZapEvent.ZapType.PRIVATE
)
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
if (recepientPK != null && recepientPost != null) {
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
val decodedPrivateZap =
privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
println(decodedPrivateZap?.toJson())
assertNotNull(decodedPrivateZap)
} else {
fail("Should not be null")
}
}
@Test
fun testKind1PrivateZap() {
val textNote = Event.Companion.fromJson(
"""{
"content": "Testing copied author. \n\nnostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
"created_at": 1682369982,
"id": "c757e1371d715c711ec9ef9740a3df6475d64b3d0af45ffcbfca08d273baf1c1",
"kind": 1,
"pubkey": "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
"sig": "1fb5b6fd980f4c2ef058d5f4f7b166c0e5fb21eff26fe9cacd87a9aa4feb344485841ebcc26a233bf8d6ea0a66acf0db2bfdb11ad1cb04bcea4cfa3e78c3eaf1",
"tags": [
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""",
true
)
val loggedIn = Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")
val privateZapRequest = LnZapRequestEvent.create(
textNote,
setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"),
loggedIn,
null,
"test",
LnZapEvent.ZapType.PRIVATE
)
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
if (recepientPK != null && recepientPost != null) {
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
println(decodedPrivateZap?.toJson())
assertNotNull(decodedPrivateZap)
} else {
fail("Should not be null")
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,17 +11,17 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TranslationsTest {
fun translatePT(text: String, translateTo: String): String? {
fun translateTo(text: String, translateTo: String): String? {
val task = LanguageTranslatorService.autoTranslate(text, emptySet(), translateTo)
return Tasks.await(task).result
}
fun assertTranslate(expected: String, input: String, translateTo: String) {
assertEquals(null, expected, translatePT(input, translateTo))
assertEquals(null, expected, translateTo(input, translateTo))
}
fun assertTranslateContains(expected: String, input: String, translateTo: String) {
val translated = translatePT(input, translateTo)!!
val translated = translateTo(input, translateTo)!!
assertTrue("'$translated' does not contain '$expected'", translated.contains(expected))
}
@@ -77,4 +77,26 @@ class TranslationsTest {
"pt"
)
}
@Test
fun testNostrEvents() {
assertTranslateContains(
"nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
"sure, nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
"en"
)
}
@Test
fun testJapaneseTranslationsOfUrl() {
assertTranslateContains(
"https://youtu.be/wMYFmCDy_Eg",
"うちの会社の小さい先輩の話 第1話「うちの会社の先輩は小さくて可愛い」\n" +
"\n" +
"https://youtu.be/wMYFmCDy_Eg\n" +
"\n" +
"先輩がうざい後輩の話と似たような話かと思ったけど、もっとオタクの妄想あるある的なものを詰め込んだやつだ。ワードとかシチュエーションとか、ヒロインのサイズ感とか。知らんけど",
"en"
)
}
}

View File

@@ -0,0 +1,8 @@
package com.vitorpamplona.amethyst.service.notifications
import com.vitorpamplona.amethyst.AccountInfo
class PushNotificationUtils {
fun init(accounts: List<AccountInfo>) {
}
}

View File

@@ -1,26 +1,27 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun TranslateableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: List<List<String>>?,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) = ExpandableRichTextViewer(
content,
canPreview,
modifier,
tags,
backgroundColor,
accountViewModel,
navController
)
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun TranslatableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) = ExpandableRichTextViewer(
content,
canPreview,
modifier,
tags,
backgroundColor,
accountViewModel,
nav
)

View File

@@ -1,14 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="org.torproject.android"/>
</queries>
<!-- Doesn't require a camera -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- To connect with relays -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- To Upload media (newer devices) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<!-- To Upload media (old devices) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- To read QRs that contain nostr:<NIP19> -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Used for SDK < 29 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<!-- To read NFCs that contain nostr:<NIP19> -->
<uses-permission android:name="android.permission.NFC" />
<!-- To know receive notifications when the app connects/disconnects from the web -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- Audio/Video Playback -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- Keeps screen on while playing videos -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Old permission to access media -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<application
@@ -16,7 +45,7 @@
android:name=".Amethyst"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/amethyst"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:enableOnBackInvokedCallback="true"
android:supportsRtl="true"
@@ -24,11 +53,14 @@
android:largeHeap="true"
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
android:localeConfig="@xml/locales_config"
tools:targetApi="33">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|screenSize|screenLayout"
android:theme="@style/Theme.Amethyst">
<intent-filter android:label="Amethyst">
@@ -43,6 +75,20 @@
<data android:scheme="nostr" />
</intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="nostr" />
</intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrwalletconnect" />
<data android:scheme="nostr+walletconnect" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
@@ -52,6 +98,18 @@
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="true"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -3,14 +3,22 @@ package com.vitorpamplona.amethyst
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Immutable
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
import com.vitorpamplona.amethyst.ui.note.Nip47URI
import fr.acinq.secp256k1.Hex
import nostr.postr.Persona
import nostr.postr.toHex
@@ -24,12 +32,10 @@ import java.util.Locale
private const val DEBUG_PLAINTEXT_PREFERENCES = false
private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
@Immutable
data class AccountInfo(
val npub: String,
val hasPrivKey: Boolean,
val current: Boolean,
val displayName: String?,
val profilePicture: String?
val hasPrivKey: Boolean = false
)
private object PrefKeys {
@@ -37,17 +43,36 @@ private object PrefKeys {
const val SAVED_ACCOUNTS = "all_saved_accounts"
const val NOSTR_PRIVKEY = "nostr_privkey"
const val NOSTR_PUBKEY = "nostr_pubkey"
const val DISPLAY_NAME = "display_name"
const val PROFILE_PICTURE_URL = "profile_picture"
const val FOLLOWING_CHANNELS = "following_channels"
const val FOLLOWING_COMMUNITIES = "following_communities"
const val HIDDEN_USERS = "hidden_users"
const val RELAYS = "relays"
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
const val LANGUAGE_PREFS = "languagePreferences"
const val TRANSLATE_TO = "translateTo"
const val ZAP_AMOUNTS = "zapAmounts"
const val REACTION_CHOICES = "reactionChoices"
const val DEFAULT_ZAPTYPE = "defaultZapType"
const val DEFAULT_FILE_SERVER = "defaultFileServer"
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_CONTACT_LIST = "latestContactList"
const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo"
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port"
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
const val WARN_ABOUT_REPORTS = "warn_about_reports"
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
const val LAST_READ_PER_ROUTE = "last_read_route_per_route"
const val AUTOMATICALLY_SHOW_IMAGES = "automatically_show_images"
const val AUTOMATICALLY_START_PLAYBACK = "automatically_start_playback"
const val THEME = "theme"
const val PREFERRED_LANGUAGE = "preferred_Language"
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
@@ -56,53 +81,73 @@ private val gson = GsonBuilder().create()
object LocalPreferences {
private const val comma = ","
private var currentAccount: String?
get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
set(npub) {
val prefs = encryptedPreferences()
prefs.edit().apply {
private var _currentAccount: String? = null
private fun currentAccount(): String? {
if (_currentAccount == null) {
_currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
}
return _currentAccount
}
private fun updateCurrentAccount(npub: String) {
if (_currentAccount != npub) {
_currentAccount = npub
encryptedPreferences().edit().apply {
putString(PrefKeys.CURRENT_ACCOUNT, npub)
}.apply()
}
}
private val savedAccounts: List<String>
get() = encryptedPreferences()
.getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf()
private var _savedAccounts: List<String>? = null
private fun savedAccounts(): List<String> {
if (_savedAccounts == null) {
_savedAccounts = encryptedPreferences()
.getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf()
}
return _savedAccounts!!
}
private fun updateSavedAccounts(accounts: List<String>) {
if (_savedAccounts != accounts) {
_savedAccounts = accounts
encryptedPreferences().edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply()
}
}
private val prefsDirPath: String
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
private fun addAccount(npub: String) {
val accounts = savedAccounts.toMutableList()
val accounts = savedAccounts().toMutableList()
if (npub !in accounts) {
accounts.add(npub)
updateSavedAccounts(accounts)
}
val prefs = encryptedPreferences()
prefs.edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply()
}
private fun setCurrentAccount(account: Account) {
val npub = account.userProfile().pubkeyNpub()
currentAccount = npub
updateCurrentAccount(npub)
addAccount(npub)
}
fun switchToAccount(npub: String) {
currentAccount = npub
updateCurrentAccount(npub)
}
/**
* Removes the account from the app level shared preferences
*/
private fun removeAccount(npub: String) {
val accounts = savedAccounts.toMutableList()
val accounts = savedAccounts().toMutableList()
if (accounts.remove(npub)) {
val prefs = encryptedPreferences()
prefs.edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply()
updateSavedAccounts(accounts)
}
}
@@ -142,11 +187,11 @@ object LocalPreferences {
removeAccount(npub)
deleteUserPreferenceFile(npub)
if (savedAccounts.isEmpty()) {
if (savedAccounts().isEmpty()) {
val appPrefs = encryptedPreferences()
appPrefs.edit().clear().apply()
} else if (currentAccount == npub) {
currentAccount = savedAccounts.elementAt(0)
} else if (currentAccount() == npub) {
updateCurrentAccount(savedAccounts().elementAt(0))
}
}
@@ -156,17 +201,8 @@ object LocalPreferences {
}
fun allSavedAccounts(): List<AccountInfo> {
return savedAccounts.map { npub ->
val prefs = encryptedPreferences(npub)
val hasPrivKey = prefs.getString(PrefKeys.NOSTR_PRIVKEY, null) != null
AccountInfo(
npub = npub,
hasPrivKey = hasPrivKey,
current = npub == currentAccount,
displayName = prefs.getString(PrefKeys.DISPLAY_NAME, null),
profilePicture = prefs.getString(PrefKeys.PROFILE_PICTURE_URL, null)
)
return savedAccounts().map { npub ->
AccountInfo(npub = npub)
}
}
@@ -176,24 +212,91 @@ object LocalPreferences {
account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) }
account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) }
putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels)
putStringSet(PrefKeys.FOLLOWING_COMMUNITIES, account.followingCommunities)
putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers)
putString(PrefKeys.RELAYS, gson.toJson(account.localRelays))
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
putString(PrefKeys.REACTION_CHOICES, gson.toJson(account.reactionChoices))
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList)
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList)
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, account.defaultDiscoveryFollowList)
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo)
putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName())
putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture())
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
putString(PrefKeys.LAST_READ_PER_ROUTE, gson.toJson(account.lastReadPerRoute))
if (account.showSensitiveContent == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
}
}.apply()
val globalPrefs = encryptedPreferences()
globalPrefs.edit().apply {
if (account.settings.automaticallyShowImages == null) {
remove(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages!!)
}
if (account.settings.automaticallyStartPlayback == null) {
remove(PrefKeys.AUTOMATICALLY_START_PLAYBACK)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback!!)
}
if (account.settings.automaticallyShowUrlPreview == null) {
remove(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview!!)
}
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
}.apply()
}
fun updateTheme(theme: Int) {
encryptedPreferences().edit().apply {
putInt(PrefKeys.THEME, theme)
}.apply()
}
fun getTheme(): Int {
encryptedPreferences().apply {
return getInt(PrefKeys.THEME, 0)
}
}
fun getPreferredLanguage(): String {
var language = ""
encryptedPreferences().apply {
language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
}
return language
}
fun loadFromEncryptedStorage(): Account? {
encryptedPreferences(currentAccount).apply {
val acc = loadFromEncryptedStorage(currentAccount())
acc?.registerObservers()
return acc
}
fun loadFromEncryptedStorage(npub: String?): Account? {
encryptedPreferences(npub).apply {
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
val localRelays = gson.fromJson(
getString(PrefKeys.RELAYS, "[]"),
@@ -202,12 +305,40 @@ object LocalPreferences {
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
val defaultHomeFollowList = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultNotificationFollowList = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultDiscoveryFollowList = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val zapAmountChoices = gson.fromJson(
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
object : TypeToken<List<Long>>() {}.type
) ?: listOf(500L, 1000L, 5000L)
val reactionChoices = gson.fromJson<List<String>>(
getString(PrefKeys.REACTION_CHOICES, "[]"),
object : TypeToken<List<String>>() {}.type
).ifEmpty { listOf("+") } ?: listOf("+")
val defaultZapType = gson.fromJson(
getString(PrefKeys.DEFAULT_ZAPTYPE, "PUBLIC"),
object : TypeToken<LnZapEvent.ZapType>() {}.type
) ?: LnZapEvent.ZapType.PUBLIC
val defaultFileServer = gson.fromJson(
getString(PrefKeys.DEFAULT_FILE_SERVER, "NOSTR_BUILD"),
object : TypeToken<ServersAvailable>() {}.type
) ?: ServersAvailable.NOSTR_BUILD
val zapPaymentRequestServer = try {
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
gson.fromJson(it, Nip47URI::class.java)
}
} catch (e: Throwable) {
e.printStackTrace()
null
}
val latestContactList = try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
Event.gson.fromJson(it, Event::class.java)
@@ -230,37 +361,90 @@ object LocalPreferences {
mapOf()
}
val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false)
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
return Account(
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
followingChannels,
hiddenUsers,
localRelays,
dontTranslateFrom,
languagePreferences,
translateTo,
zapAmountChoices,
hideDeleteRequestInfo,
latestContactList
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
} else {
null
}
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
val lastReadPerRoute = try {
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
gson.fromJson(
it,
object : TypeToken<Map<String, Long>>() {}.type
) as Map<String, Long>
} ?: mapOf()
} catch (e: Throwable) {
e.printStackTrace()
mapOf()
}
val settings = Settings()
encryptedPreferences().apply {
settings.automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) {
getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false)
} else {
null
}
settings.automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) {
getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false)
} else {
null
}
settings.automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) {
getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false)
} else {
null
}
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
}
val a = Account(
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels = followingChannels,
followingCommunities = followingCommunities,
hiddenUsers = hiddenUsers,
localRelays = localRelays,
dontTranslateFrom = dontTranslateFrom,
languagePreferences = languagePreferences,
translateTo = translateTo,
zapAmountChoices = zapAmountChoices,
reactionChoices = reactionChoices,
defaultZapType = defaultZapType,
defaultFileServer = defaultFileServer,
defaultHomeFollowList = defaultHomeFollowList,
defaultStoriesFollowList = defaultStoriesFollowList,
defaultNotificationFollowList = defaultNotificationFollowList,
defaultDiscoveryFollowList = defaultDiscoveryFollowList,
zapPaymentRequest = zapPaymentRequestServer,
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
backupContactList = latestContactList,
proxy = proxy,
proxyPort = proxyPort,
showSensitiveContent = showSensitiveContent,
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
settings = settings
)
}
}
fun saveLastRead(route: String, timestampInSecs: Long) {
encryptedPreferences(currentAccount).edit().apply {
putLong(PrefKeys.LAST_READ(route), timestampInSecs)
}.apply()
}
fun loadLastRead(route: String): Long {
encryptedPreferences(currentAccount).run {
return getLong(PrefKeys.LAST_READ(route), 0)
return a
}
}
fun migrateSingleUserPrefs() {
if (currentAccount != null) return
if (currentAccount() != null) return
val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return
val npub = Hex.decode(pubkey).toNpub()
@@ -289,14 +473,14 @@ object LocalPreferences {
stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) }
stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) }
userPrefs.putBoolean(
PrefKeys.HIDE_DELETE_REQUEST_INFO,
appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false)
PrefKeys.HIDE_DELETE_REQUEST_DIALOG,
appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
)
}.apply()
}
encryptedPreferences().edit().clear().apply()
addAccount(npub)
currentAccount = npub
updateCurrentAccount(npub)
}
}

View File

@@ -0,0 +1,133 @@
package com.vitorpamplona.amethyst
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.LruCache
import androidx.core.net.toUri
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.Player.STATE_READY
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import com.vitorpamplona.amethyst.ui.MainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.abs
class MultiPlayerPlaybackManager(
private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null,
private val cachedPositions: VideoViewedPositionCache
) {
// protects from LruCache killing playing sessions
private val playingMap = mutableMapOf<String, MediaSession>()
private val cache =
object : LruCache<String, MediaSession>(10) { // up to 10 videos in the screen at the same time
override fun entryRemoved(
evicted: Boolean,
key: String?,
oldValue: MediaSession?,
newValue: MediaSession?
) {
super.entryRemoved(evicted, key, oldValue, newValue)
if (!playingMap.contains(key)) {
oldValue?.let {
it.player.release()
it.release()
}
}
}
}
private fun getCallbackIntent(callbackUri: String, applicationContext: Context): PendingIntent {
return PendingIntent.getActivity(
applicationContext,
0,
Intent(Intent.ACTION_VIEW, callbackUri.toUri(), applicationContext, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getMediaSession(id: String, uri: String, callbackUri: String?, context: Context, applicationContext: Context): MediaSession {
val existingSession = playingMap.get(id) ?: cache.get(id)
if (existingSession != null) return existingSession
val player = ExoPlayer.Builder(context).run {
dataSourceFactory?.let { setMediaSourceFactory(it) }
build()
}
player.apply {
repeatMode = Player.REPEAT_MODE_ALL
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
volume = 0f
}
val mediaSession = MediaSession.Builder(context, player).run {
callbackUri?.let {
setSessionActivity(getCallbackIntent(it, applicationContext))
}
setId(id)
build()
}
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
player.setWakeMode(C.WAKE_MODE_NETWORK)
playingMap.put(id, mediaSession)
} else {
player.setWakeMode(C.WAKE_MODE_NONE)
cache.put(id, mediaSession)
playingMap.remove(id, mediaSession)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
STATE_IDLE -> {
if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.add(uri, player.currentPosition)
}
}
STATE_READY -> {
cachedPositions.get(uri)?.let { lastPosition ->
if (abs(player.currentPosition - lastPosition) > 5 * 60) {
player.seekTo(lastPosition)
}
}
}
else -> {
if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.add(uri, player.currentPosition)
}
}
}
}
})
cache.put(id, mediaSession)
return mediaSession
}
fun releaseAppPlayers() {
GlobalScope.launch(Dispatchers.Main) {
cache.evictAll()
playingMap.forEach {
it.value.player.release()
it.value.release()
}
playingMap.clear()
}
}
fun playingContent(): Collection<MediaSession> {
return playingMap.values
}
}

View File

@@ -1,68 +0,0 @@
package com.vitorpamplona.amethyst
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
object NotificationCache {
val lastReadByRoute = mutableMapOf<String, Long>()
fun markAsRead(route: String, timestampInSecs: Long) {
val lastTime = lastReadByRoute[route]
if (lastTime == null || timestampInSecs > lastTime) {
lastReadByRoute.put(route, timestampInSecs)
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
LocalPreferences.saveLastRead(route, timestampInSecs)
live.invalidateData()
}
}
}
fun load(route: String): Long {
var lastTime = lastReadByRoute[route]
if (lastTime == null) {
lastTime = LocalPreferences.loadLastRead(route)
lastReadByRoute[route] = lastTime
}
return lastTime
}
// Observers line up here.
val live: NotificationLiveData = NotificationLiveData(this)
}
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
// Refreshes observers in batches.
var handlerWaiting = AtomicBoolean()
fun invalidateData() {
if (!hasActiveObservers()) return
if (handlerWaiting.getAndSet(true)) return
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
try {
delay(100)
refresh()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
}
}
}
fun refresh() {
postValue(NotificationState(cache))
}
}
class NotificationState(val cache: NotificationCache)

View File

@@ -0,0 +1,11 @@
package com.vitorpamplona.amethyst
object OptOutFromFilters {
var warnAboutPostsWithReports: Boolean = true
var filterSpamFromStrangers: Boolean = true
fun start(warnAboutReports: Boolean, filterSpam: Boolean) {
warnAboutPostsWithReports = warnAboutReports
filterSpamFromStrangers = filterSpam
}
}

View File

@@ -0,0 +1,40 @@
package com.vitorpamplona.amethyst
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
object PlaybackClientController {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun prepareController(
controllerID: String,
videoUri: String,
callbackUri: String?,
context: Context,
onReady: (MediaController) -> Unit
) {
// creating a bundle object
// creating a bundle object
val bundle = Bundle()
bundle.putString("id", controllerID)
bundle.putString("uri", videoUri)
bundle.putString("callbackUri", callbackUri)
val sessionTokenLocal = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val controllerFuture = MediaController
.Builder(context, sessionTokenLocal)
.setConnectionHints(bundle)
.buildAsync()
controllerFuture.addListener(
{
val controller = controllerFuture.get()
onReady(controller)
},
MoreExecutors.directExecutor()
)
}
}

View File

@@ -0,0 +1,124 @@
package com.vitorpamplona.amethyst
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.service.HttpClient
@UnstableApi // Extend MediaSessionService
class PlaybackService : MediaSessionService() {
private var videoViewedPositionCache = VideoViewedPositionCache()
private var managerHls: MultiPlayerPlaybackManager? = null
private var managerProgressive: MultiPlayerPlaybackManager? = null
private var managerLocal: MultiPlayerPlaybackManager? = null
fun newHslDataSource(): MediaSource.Factory {
return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient()))
}
fun newProgressiveDataSource(): MediaSource.Factory {
return ProgressiveMediaSource.Factory(VideoCache.get(applicationContext, HttpClient.getHttpClient()))
}
// Create your Player and MediaSession in the onCreate lifecycle event
@OptIn(UnstableApi::class)
override fun onCreate() {
super.onCreate()
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
managerLocal = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
// Stop all videos and recreates all managers when the proxy changes.
HttpClient.proxyChangeListeners.add {
val toDestroyHls = managerHls
val toDestroyProgressive = managerProgressive
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
toDestroyHls?.releaseAppPlayers()
toDestroyProgressive?.releaseAppPlayers()
}
setMediaNotificationProvider(
DefaultMediaNotificationProvider.Builder(applicationContext).build()
)
}
override fun onDestroy() {
managerHls?.releaseAppPlayers()
managerLocal?.releaseAppPlayers()
managerProgressive?.releaseAppPlayers()
super.onDestroy()
}
fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? {
return if (fileName.startsWith("file")) {
managerLocal
} else if (fileName.endsWith("m3u8")) {
managerHls
} else {
managerProgressive
}
}
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
// Updates any new player ready
super.onUpdateNotification(session, startInForegroundRequired)
// Overrides the notification with any player actually playing
managerHls?.playingContent()?.forEach {
if (it.player.isPlaying) {
super.onUpdateNotification(it, startInForegroundRequired)
}
}
managerLocal?.playingContent()?.forEach {
if (it.player.isPlaying) {
super.onUpdateNotification(session, startInForegroundRequired)
}
}
managerProgressive?.playingContent()?.forEach {
if (it.player.isPlaying) {
super.onUpdateNotification(session, startInForegroundRequired)
}
}
// Overrides again with playing with audio
managerHls?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(it, startInForegroundRequired)
}
}
managerLocal?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(session, startInForegroundRequired)
}
}
managerProgressive?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(session, startInForegroundRequired)
}
}
}
// Return a MediaSession to link with the MediaController that is making
// this request.
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
val id = controllerInfo.connectionHints.getString("id") ?: return null
val uri = controllerInfo.connectionHints.getString("uri") ?: return null
val callbackUri = controllerInfo.connectionHints.getString("callbackUri")
val manager = getAppropriateMediaSessionManager(uri)
return manager?.getMediaSession(id, uri, callbackUri, context = this, applicationContext = applicationContext)
}
}

View File

@@ -1,31 +1,65 @@
package com.vitorpamplona.amethyst
import android.content.Context
import android.os.Build
import android.util.Log
import coil.Coil
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
object ServiceManager {
private var account: Account? = null
fun start(account: Account) {
fun start(account: Account, context: Context) {
this.account = account
start()
start(context)
}
fun start() {
@Synchronized
fun start(context: Context) {
Log.d("ServiceManager", "Starting Relay Services")
val myAccount = account
// Resets Proxy Use
HttpClient.start(account)
OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true)
Coil.setImageLoader {
ImageLoader.Builder(context).components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(SvgDecoder.Factory())
} // .logger(DebugLogger())
.okHttpClient { HttpClient.getHttpClient() }
.respectCacheHeaders(false)
.build()
}
if (myAccount != null) {
Client.connect(myAccount.activeRelays() ?: myAccount.convertLocalRelays())
@@ -33,34 +67,43 @@ object ServiceManager {
NostrAccountDataSource.account = myAccount
NostrHomeDataSource.account = myAccount
NostrChatroomListDataSource.account = myAccount
NostrVideoDataSource.account = myAccount
NostrDiscoveryDataSource.account = myAccount
ImageUploader.account = myAccount
// Notification Elements
NostrAccountDataSource.start()
NostrHomeDataSource.start()
NostrAccountDataSource.start()
NostrChatroomListDataSource.start()
NostrDiscoveryDataSource.start()
NostrVideoDataSource.start()
// More Info Data Sources
NostrSingleEventDataSource.start()
NostrSingleChannelDataSource.start()
NostrSingleUserDataSource.start()
} else {
// if not logged in yet, start a basic service wit default relays
Client.connect(Constants.convertDefaultRelays())
}
}
fun pause() {
Log.d("ServiceManager", "Pausing Relay Services")
NostrAccountDataSource.stop()
NostrHomeDataSource.stop()
NostrChannelDataSource.stop()
NostrChatroomDataSource.stop()
NostrChatroomListDataSource.stop()
NostrDiscoveryDataSource.stop()
NostrGlobalDataSource.stop()
NostrCommunityDataSource.stop()
NostrHashtagDataSource.stop()
NostrSearchEventOrUserDataSource.stop()
NostrSingleChannelDataSource.stop()
NostrSingleEventDataSource.stop()
NostrSingleUserDataSource.stop()
NostrThreadDataSource.stop()
NostrUserProfileDataSource.stop()
NostrVideoDataSource.stop()
Client.disconnect()
}
@@ -71,6 +114,7 @@ object ServiceManager {
account?.let {
LocalCache.pruneOldAndHiddenMessages(it)
LocalCache.pruneHiddenMessages(it)
LocalCache.pruneContactLists(it)
// LocalCache.pruneNonFollows(it)
}
}

View File

@@ -1,13 +1,15 @@
package com.vitorpamplona.amethyst
import android.content.Context
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import okhttp3.OkHttpClient
object VideoCache {
@UnstableApi object VideoCache {
var exoPlayerCacheSize: Long = 90 * 1024 * 1024 // 90MB
@@ -18,22 +20,33 @@ object VideoCache {
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
fun get(context: Context): CacheDataSource.Factory {
if (!this::simpleCache.isInitialized) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
@Synchronized
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun init(context: Context, client: OkHttpClient) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
simpleCache = SimpleCache(
context.cacheDir,
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider
simpleCache = SimpleCache(
context.cacheDir,
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider
)
renewCacheFactory(client)
}
// This method should be called when proxy setting changes.
fun renewCacheFactory(client: OkHttpClient) {
cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(simpleCache)
.setUpstreamDataSourceFactory(
OkHttpDataSource.Factory(client)
)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}
cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(simpleCache)
.setUpstreamDataSourceFactory(
DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
fun get(context: Context, client: OkHttpClient): CacheDataSource.Factory {
if (!this::simpleCache.isInitialized) {
init(context, client)
}
return cacheDataSourceFactory

View File

@@ -0,0 +1,15 @@
package com.vitorpamplona.amethyst
import android.util.LruCache
class VideoViewedPositionCache {
val cachedPosition = LruCache<String, Long>(100)
fun add(uri: String, position: Long) {
cachedPosition.put(uri, position)
}
fun get(uri: String): Long? {
return cachedPosition.get(uri)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,15 @@ package com.vitorpamplona.amethyst.model
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.Event
import kotlinx.coroutines.CoroutineScope
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>)
@@ -19,13 +18,21 @@ class AntiSpamFilter {
val recentMessages = LruCache<Int, String>(1000)
val spamMessages = LruCache<Int, Spammer>(1000)
@Synchronized
fun isSpam(event: Event): Boolean {
fun isSpam(event: Event, relay: Relay?): Boolean {
checkNotInMainThread()
if (!OptOutFromFilters.filterSpamFromStrangers) return false
val idHex = event.id
// if short message, ok
// The idea here is to avoid considering repeated "GM" messages spam.
if (event.content.length < 50) return false
// if the message is actually short but because it cites a user/event, the nostr: string is really long, make it ok.
// The idea here is to avoid considering repeated "@Bot, command" messages spam, while still blocking repeated "lnbc..." invoices or fishing urls
if (event.content.length < 180 && Nip19.nip19regex.matcher(event.content).find()) return false
// double list strategy:
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
@@ -33,18 +40,13 @@ class AntiSpamFilter {
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
Log.w("Potential SPAM Message for sharing", "${Nip19.createNEvent(event.id, event.pubKey, event.kind, null)}")
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}")
// Log down offenders
if (spamMessages.get(hash) == null) {
spamMessages.put(hash, Spammer(event.pubKey, setOf(recentMessages[hash], event.id)))
liveSpam.invalidateData()
} else {
val spammer = spamMessages.get(hash)
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
logOffender(hash, event)
liveSpam.invalidateData()
}
liveSpam.invalidateData()
return true
}
@@ -54,33 +56,35 @@ class AntiSpamFilter {
return false
}
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
}
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
// Refreshes observers in batches.
var handlerWaiting = AtomicBoolean()
fun invalidateData() {
if (!hasActiveObservers()) return
if (handlerWaiting.getAndSet(true)) return
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
try {
delay(100)
refresh()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
}
@Synchronized
private fun logOffender(hashCode: Int, event: Event) {
if (spamMessages.get(hashCode) == null) {
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
} else {
val spammer = spamMessages.get(hashCode)
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
}
}
private fun refresh() {
postValue(AntiSpamState(cache))
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
}
@Stable
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(AntiSpamState(cache))
}
}
}
}

View File

@@ -1,49 +1,128 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ConcurrentHashMap
class Channel(val idHex: String) {
var creator: User? = null
@Stable
class PublicChatChannel(idHex: String) : Channel(idHex) {
var info = ChannelCreateEvent.ChannelData(null, null, null)
var updatedMetadataAt: Long = 0
val notes = ConcurrentHashMap<HexKey, Note>()
fun id() = Hex.decode(idHex)
fun idNote() = id().toNote()
fun idDisplayNote() = idNote().toShortenHex()
fun toBestDisplayName(): String {
return info.name ?: idDisplayNote()
}
fun addNote(note: Note) {
notes[note.idHex] = note
}
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
this.creator = creator
this.info = channelInfo
this.updatedMetadataAt = updatedAt
live.refresh()
live.invalidateData()
}
fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) info.picture = null
return info.picture
override fun toBestDisplayName(): String {
return info.name ?: super.toBestDisplayName()
}
fun anyNameStartsWith(prefix: String): Boolean {
override fun summary(): String? {
return info.about
}
override fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) return super.profilePicture()
return info.picture ?: super.profilePicture()
}
override fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(info.name, info.about)
.filter { it.startsWith(prefix, true) }.isNotEmpty()
.filter { it.contains(prefix, true) }.isNotEmpty()
}
}
@Stable
class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
var info: LiveActivitiesEvent? = null
override fun idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
fun address() = address
fun updateChannelInfo(creator: User, channelInfo: LiveActivitiesEvent, updatedAt: Long) {
this.info = channelInfo
super.updateChannelInfo(creator, updatedAt)
}
override fun toBestDisplayName(): String {
return info?.title() ?: super.toBestDisplayName()
}
override fun summary(): String? {
return info?.summary()
}
override fun profilePicture(): String? {
return info?.image()?.ifBlank { null }
}
override fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(info?.title(), info?.summary())
.filter { it.contains(prefix, true) }.isNotEmpty()
}
}
@Stable
abstract class Channel(val idHex: String) {
var creator: User? = null
var updatedMetadataAt: Long = 0
val notes = ConcurrentHashMap<HexKey, Note>()
open fun id() = Hex.decode(idHex)
open fun idNote() = id().toNote()
open fun idDisplayNote() = idNote().toShortenHex()
open fun toBestDisplayName(): String {
return idDisplayNote()
}
open fun summary(): String? {
return null
}
open fun creatorName(): String? {
return creator?.toBestDisplayName()
}
open fun profilePicture(): String? {
return creator?.profilePicture()
}
open fun updateChannelInfo(creator: User, updatedAt: Long) {
this.creator = creator
this.updatedMetadataAt = updatedAt
live.invalidateData()
}
fun addNote(note: Note) {
notes[note.idHex] = note
}
fun removeNote(note: Note) {
notes.remove(note.idHex)
}
fun removeNote(noteHex: String) {
notes.remove(noteHex)
}
abstract fun anyNameStartsWith(prefix: String): Boolean
// Observers line up here.
val live: ChannelLiveData = ChannelLiveData(this)
@@ -51,7 +130,7 @@ class Channel(val idHex: String) {
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
val important = notes.values
.filter { it.author?.let { it1 -> account.isHidden(it1) } == false }
.sortedBy { it.createdAt() }
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
.take(1000)
.toSet()
@@ -67,18 +146,36 @@ class Channel(val idHex: String) {
}
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
fun refresh() {
postValue(ChannelState(channel))
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(ChannelState(channel))
}
}
}
override fun onActive() {
super.onActive()
NostrSingleChannelDataSource.add(channel.idHex)
if (channel is PublicChatChannel) {
NostrSingleChannelDataSource.add(channel.idHex)
} else {
NostrSingleChannelDataSource.add(channel.idHex)
}
}
override fun onInactive() {
super.onInactive()
NostrSingleChannelDataSource.remove(channel.idHex)
if (channel is PublicChatChannel) {
NostrSingleChannelDataSource.remove(channel.idHex)
} else {
NostrSingleChannelDataSource.remove(channel.idHex)
}
}
}

View File

@@ -0,0 +1,34 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
fun checkForHashtagWithIcon(tag: String, primary: Color): HashtagIcon? {
return when (tag.lowercase()) {
"bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> HashtagIcon(R.drawable.ht_btc, "Bitcoin", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp))
"nostr", "nostrich", "nostriches" -> HashtagIcon(R.drawable.ht_nostr, "Nostr", Color.Unspecified, Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp))
"lightning", "lightningnetwork" -> HashtagIcon(R.drawable.ht_lightning, "Lightning", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
"zap", "zaps", "zapper", "zappers", "zapping", "zapped", "zapathon", "zapraiser", "zaplife", "zapchain" -> HashtagIcon(R.drawable.zap, "Zap", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
"amethyst" -> HashtagIcon(R.drawable.amethyst, "Amethyst", Color.Unspecified, Modifier.padding(3.dp, 2.dp, 0.dp, 0.dp))
"onyx" -> HashtagIcon(R.drawable.black_heart, "Onyx", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
"cashu", "ecash", "nut", "nuts", "deeznuts" -> HashtagIcon(R.drawable.cashu, "Cashu", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
"plebs", "pleb", "plebchain" -> HashtagIcon(R.drawable.plebs, "Pleb", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 1.dp))
"coffee", "coffeechain", "cafe" -> HashtagIcon(R.drawable.coffee, "Coffee", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp))
"skullofsatoshi" -> HashtagIcon(R.drawable.skull, "SkullofSatoshi", Color.Unspecified, Modifier.padding(2.dp, 1.dp, 0.dp, 0.dp))
"grownostr", "gardening", "garden" -> HashtagIcon(R.drawable.grownostr, "GrowNostr", Color.Unspecified, Modifier.padding(0.dp, 1.dp, 0.dp, 1.dp))
"footstr" -> HashtagIcon(R.drawable.footstr, "Footstr", Color.Unspecified, Modifier.padding(1.dp, 1.dp, 0.dp, 0.dp))
"tunestr", "music", "nowplaying" -> HashtagIcon(R.drawable.tunestr, "Tunestr", primary, Modifier.padding(0.dp, 3.dp, 0.dp, 1.dp))
"weed", "weedstr", "420", "cannabis", "marijuana" -> HashtagIcon(R.drawable.weed, "Weed", Color.Unspecified, Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp))
else -> null
}
}
@Immutable
class HashtagIcon(
val icon: Int,
val description: String,
val color: Color,
val modifier: Modifier
)

View File

@@ -29,7 +29,7 @@ fun ByteArray.toHexKey(): HexKey {
return toHex()
}
fun HexKey.toByteArray(): ByteArray {
fun HexKey.hexToByteArray(): ByteArray {
return Hex.decode(this)
}
@@ -38,18 +38,35 @@ fun HexKey.toDisplayHexKey(): String {
}
fun decodePublicKey(key: String): ByteArray {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray()
return if (key.startsWith("nsec")) {
Persona(privKey = key.bechToBytes()).pubKey
} else if (key.startsWith("npub")) {
key.bechToBytes()
} else if (key.startsWith("note")) {
key.bechToBytes()
} else { // if (pattern.matcher(key).matches()) {
// } else {
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key)
}
}
fun decodePublicKeyAsHexOrNull(key: String): HexKey? {
return try {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex
if (key.startsWith("nsec")) {
Persona(privKey = key.bechToBytes()).pubKey.toHexKey()
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key).toHexKey()
}
} catch (e: Exception) {
null
}
}
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
@@ -84,15 +101,15 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(pubkeyRelay, restOfWord)
} else if (key.startsWith("nevent", true)) {
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(noteRelayId, restOfWord)
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(address, "") // no way to know when they address ends and dirt begins
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
}
} catch (e: Exception) {
e.printStackTrace()

View File

@@ -1,35 +1,47 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
@Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) {
override fun idNote() = address.toNAddr()
override fun toNEvent() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address
override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt()
override fun createdAt(): Long? {
if (event == null) return null
val publishedAt = (event as? LongTextNoteEvent)?.publishedAt() ?: Long.MAX_VALUE
val lastCreatedAt = event?.createdAt() ?: Long.MAX_VALUE
return minOf(publishedAt, lastCreatedAt)
}
}
@Stable
open class Note(val idHex: String) {
// These fields are only available after the Text Note event is received.
// They are immutable after that.
@@ -40,7 +52,7 @@ open class Note(val idHex: String) {
// These fields are updated every time an event related to this note is received.
var replies = setOf<Note>()
private set
var reactions = setOf<Note>()
var reactions = mapOf<String, Set<Note>>()
private set
var boosts = setOf<Note>()
private set
@@ -48,26 +60,45 @@ open class Note(val idHex: String) {
private set
var zaps = mapOf<Note, Note?>()
private set
var zapPayments = mapOf<Note, Note?>()
private set
var relays = setOf<String>()
private set
var lastReactionsDownloadTime: Long? = null
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
fun id() = Hex.decode(idHex)
open fun idNote() = id().toNote()
open fun idDisplayNote() = idNote().toShortenHex()
fun channel(): Channel? {
val channelHex =
(event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.let { it.id }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
open fun toNEvent(): String {
return Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
}
open fun address() = (event as? LongTextNoteEvent)?.address()
fun toNostrUri(): String {
return "nostr:${toNEvent()}"
}
open fun idDisplayNote() = idNote().toShortenHex()
fun channelHex(): HexKey? {
return if (event is ChannelMessageEvent ||
event is ChannelMetadataEvent ||
event is ChannelCreateEvent ||
event is LiveActivitiesChatMessageEvent ||
event is LiveActivitiesEvent
) {
(event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
} else {
null
}
}
open fun address(): ATag? = null
open fun createdAt() = event?.createdAt()
@@ -89,7 +120,7 @@ open class Note(val idHex: String) {
*/
fun replyLevelSignature(cachedSignatures: MutableMap<Note, String> = mutableMapOf()): String {
val replyTo = replyTo
if (replyTo == null || replyTo.isEmpty()) {
if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
}
@@ -102,7 +133,7 @@ open class Note(val idHex: String) {
fun replyLevel(cachedLevels: MutableMap<Note, Int> = mutableMapOf()): Int {
val replyTo = replyTo
if (replyTo == null || replyTo.isEmpty()) {
if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
return 0
}
@@ -127,8 +158,21 @@ open class Note(val idHex: String) {
liveSet?.boosts?.invalidateData()
}
fun removeReaction(note: Note) {
reactions = reactions - note
liveSet?.reactions?.invalidateData()
val tags = note.event?.tags() ?: emptyList()
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
reactions[reaction]?.let {
val newList = it.minus(note)
if (newList.isEmpty()) {
reactions = reactions.minus(reaction)
} else {
reactions = reactions + Pair(reaction, newList)
}
liveSet?.reactions?.invalidateData()
}
}
}
fun removeReport(deleteNote: Note) {
@@ -153,6 +197,17 @@ open class Note(val idHex: String) {
}
}
fun removeZapPayment(note: Note) {
if (zapPayments[note] != null) {
zapPayments = zapPayments.minus(note)
liveSet?.zaps?.invalidateData()
} else if (zapPayments.containsValue(note)) {
val toRemove = zapPayments.filterValues { it == note }
zapPayments = zapPayments.minus(toRemove.keys)
liveSet?.zaps?.invalidateData()
}
}
fun addBoost(note: Note) {
if (note !in boosts) {
boosts = boosts + note
@@ -160,19 +215,71 @@ open class Note(val idHex: String) {
}
}
fun addZap(zapRequest: Note, zap: Note?) {
@Synchronized
private fun innerAddZap(zapRequest: Note, zap: Note?): Boolean {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData()
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
return true
} else if (zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData()
return true
}
return false
}
fun addZap(zapRequest: Note, zap: Note?) {
checkNotInMainThread()
if (zapRequest !in zaps.keys) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
} else if (zaps[zapRequest] == null) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
}
}
@Synchronized
private fun innerAddZapPayment(zapPaymentRequest: Note, zapPayment: Note?): Boolean {
if (zapPaymentRequest !in zapPayments.keys) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
return true
} else if (zapPayments[zapPaymentRequest] == null) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
return true
}
return false
}
fun addZapPayment(zapPaymentRequest: Note, zapPayment: Note?) {
checkNotInMainThread()
if (zapPaymentRequest !in zapPayments.keys) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
} else if (zapPayments[zapPaymentRequest] == null) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
}
}
fun addReaction(note: Note) {
if (note !in reactions) {
reactions = reactions + note
val tags = note.event?.tags() ?: emptyList()
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
if (reaction !in reactions.keys) {
reactions = reactions + Pair(reaction, setOf(note))
liveSet?.reactions?.invalidateData()
} else if (reactions[reaction]?.contains(note) == false) {
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
liveSet?.reactions?.invalidateData()
}
}
@@ -196,17 +303,82 @@ open class Note(val idHex: String) {
}
}
fun isZappedBy(user: User): Boolean {
fun isZappedBy(user: User, account: Account): Boolean {
// Zaps who the requester was the user
return zaps.any { it.key.author == user }
return zaps.any {
it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
} || zapPayments.any {
val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent
val response = if (zapResponseEvent != null) {
account.decryptZapPaymentResponseEvent(zapResponseEvent)
} else {
null
}
response is PayInvoiceSuccessResponse && account.isNIP47Author(zapResponseEvent?.requestAuthor())
}
}
fun isReactedBy(user: User): Boolean {
return reactions.any { it.author == user }
fun publicZapAuthors(): Set<User> {
// Zaps who the requester was the user
return zaps.mapNotNull {
it.key.author
}.toSet()
}
fun publicZapAuthorHexes(): Set<HexKey> {
// Zaps who the requester was the user
return zaps.mapNotNull {
it.key.author?.pubkeyHex
}.toSet()
}
fun reactionAuthors(): Set<User> {
// Zaps who the requester was the user
return reactions.values.map {
it.mapNotNull { it.author }
}.flatten().toSet()
}
fun reactionAuthorHexes(): Set<HexKey> {
// Zaps who the requester was the user
return reactions.values.map {
it.mapNotNull { it.author?.pubkeyHex }
}.flatten().toSet()
}
fun replyAuthorHexes(): Set<HexKey> {
// Zaps who the requester was the user
return replies.mapNotNull {
it.author?.pubkeyHex
}.toSet()
}
fun replyAuthors(): Set<User> {
// Zaps who the requester was the user
return replies.mapNotNull {
it.author
}.toSet()
}
fun boostAuthors(): Set<User> {
// Zaps who the requester was the user
return boosts.mapNotNull {
it.author
}.toSet()
}
fun getReactionBy(user: User): String? {
return reactions.firstNotNullOfOrNull {
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
it.key
} else {
null
}
}
}
fun isBoostedBy(user: User): Boolean {
return boosts.any { it.author == user }
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
}
fun reportsBy(user: User): Set<Note> {
@@ -227,65 +399,98 @@ open class Note(val idHex: String) {
}.flatten()
}
fun zappedAmount(): BigDecimal {
return zaps.mapNotNull { it.value?.event }
fun countReactions(): Int {
return reactions.values.sumOf { it.size }
}
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
// Regular Zap Receipts
val completedZaps = zaps.asSequence()
.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>()
.filter { it.amount != null }
.associate {
it.lnInvoice() to it.amount
}
.toMap()
val completedPayments = if (privKey != null && walletServicePubkey != null) {
// Payments confirmed by the User's Wallet
zapPayments
.asSequence()
.filter {
val response = (it.value?.event as? LnZapPaymentResponseEvent)?.response(privKey, walletServicePubkey)
response is PayInvoiceSuccessResponse
}
.associate {
val lnInvoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice(privKey, walletServicePubkey)
val amount = try {
if (lnInvoice == null) {
null
} else {
LnInvoiceUtil.getAmountInSats(lnInvoice)
}
} catch (e: java.lang.Exception) {
null
}
lnInvoice to amount
}
.toMap()
} else {
emptyMap()
}
return (completedZaps + completedPayments).values.filterNotNull().sumOf { it }
}
fun hasPledgeBy(user: User): Boolean {
return replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.any {
val pledgeValue = try {
BigDecimal(it.event?.content())
} catch (e: Exception) {
null
// do nothing if it can't convert to bigdecimal
}
pledgeValue != null && it.author == user
}
}
fun pledgedAmountByOthers(): BigDecimal {
return replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.mapNotNull {
it.amount
}.sumOf { it }
try {
BigDecimal(it.event?.content())
} catch (e: Exception) {
null
// do nothing if it can't convert to bigdecimal
}
}
.sumOf { it }
}
fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24 * 60 * 60
val dayAgo = TimeUtils.oneDayAgo()
return reports.isNotEmpty() ||
(
author?.reports?.values?.filter {
author?.reports?.values?.any {
it.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null
}?.isNotEmpty() ?: false
} ?: false
)
}
fun directlyCiteUsersHex(): Set<HexKey> {
val matcher = tagSearch.matcher(event?.content() ?: "")
val returningList = mutableSetOf<String>()
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { event?.tags()?.get(it.toInt()) }
if (tag != null && tag[0] == "p") {
returningList.add(tag[1])
}
} catch (e: Exception) {
}
}
return returningList
}
fun directlyCiteUsers(): Set<User> {
val matcher = tagSearch.matcher(event?.content() ?: "")
val returningList = mutableSetOf<User>()
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { event?.tags()?.get(it.toInt()) }
if (tag != null && tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let {
returningList.add(it)
}
}
} catch (e: Exception) {
}
}
return returningList
}
fun directlyCites(userProfile: User): Boolean {
return author == userProfile ||
(userProfile in directlyCiteUsers()) ||
(event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) ||
(event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true)
}
fun isNewThread(): Boolean {
return event is RepostEvent || replyTo == null || replyTo?.size == 0
return (
event is RepostEvent ||
event is GenericRepostEvent ||
replyTo == null ||
replyTo?.size == 0
) &&
event !is ChannelMessageEvent &&
event !is LiveActivitiesChatMessageEvent
}
fun hasZapped(loggedIn: User): Boolean {
@@ -297,18 +502,57 @@ open class Note(val idHex: String) {
}
fun reactedBy(loggedIn: User, content: String): List<Note> {
return reactions.filter { it.author == loggedIn && it.event?.content() == content }
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
}
fun reactedBy(loggedIn: User): List<String> {
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
}
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val currentTime = Date().time / 1000
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo() } != null // 5 minute protection
}
fun boostedBy(loggedIn: User): List<Note> {
return boosts.filter { it.author == loggedIn }
}
fun moveAllReferencesTo(note: AddressableNote) {
// migrates these comments to a new version
replies.forEach {
note.addReply(it)
it.replyTo = it.replyTo?.updated(this, note)
}
reactions.forEach {
it.value.forEach {
note.addReaction(it)
it.replyTo = it.replyTo?.updated(this, note)
}
}
boosts.forEach {
note.addBoost(it)
it.replyTo = it.replyTo?.updated(this, note)
}
reports.forEach {
it.value.forEach {
note.addReport(it)
it.replyTo = it.replyTo?.updated(this, note)
}
}
zaps.forEach {
note.addZap(it.key, it.value)
it.key.replyTo = it.key.replyTo?.updated(this, note)
it.value?.replyTo = it.value?.replyTo?.updated(this, note)
}
replyTo = null
replies = emptySet()
reactions = emptyMap()
boosts = emptySet()
reports = emptyMap()
zaps = emptyMap()
}
var liveSet: NoteLiveSet? = null
fun live(): NoteLiveSet {
@@ -323,6 +567,22 @@ open class Note(val idHex: String) {
liveSet = null
}
}
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
if (event == null) return false
val isBoostedNoteHidden = if (event is GenericRepostEvent || event is RepostEvent || event is CommunityPostApprovalEvent) {
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else {
false
}
val isSensitive = event?.isSensitive() ?: false
return isBoostedNoteHidden ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false)
}
}
class NoteLiveSet(u: Note) {
@@ -349,29 +609,20 @@ class NoteLiveSet(u: Note) {
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches.
var handlerWaiting = AtomicBoolean()
private val bundler = BundledUpdate(500, Dispatchers.IO)
fun invalidateData() {
if (!hasActiveObservers()) return
if (handlerWaiting.getAndSet(true)) return
checkNotInMainThread()
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
try {
delay(100)
refresh()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(NoteState(note))
}
}
}
private fun refresh() {
postValue(NoteState(note))
}
override fun onActive() {
super.onActive()
if (note is AddressableNote) {
@@ -391,4 +642,5 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
}
}
@Immutable
class NoteState(val note: Note)

View File

@@ -0,0 +1,75 @@
package com.vitorpamplona.amethyst.model
class ParticipantListBuilder {
private fun addFollowsThatDirectlyParticipateOnToSet(baseNote: Note, followingSet: Set<HexKey>?, set: MutableSet<User>) {
baseNote.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
}
// Breaks these searchers down to avoid the memory use of creating multiple lists
baseNote.replies.forEach { reply ->
reply.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
}
}
baseNote.boosts.forEach { boost ->
boost.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
}
}
baseNote.zaps.forEach { zapPair ->
zapPair.key.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
}
}
baseNote.reactions.forEach { reactionSet ->
reactionSet.value.forEach { reaction ->
reaction.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
}
}
}
}
fun followsThatParticipateOnDirect(baseNote: Note?, followingSet: Set<HexKey>?): Set<User> {
if (baseNote == null) return mutableSetOf()
val set = mutableSetOf<User>()
addFollowsThatDirectlyParticipateOnToSet(baseNote, followingSet, set)
return set
}
fun followsThatParticipateOn(baseNote: Note?, followingSet: Set<HexKey>?): Set<User> {
if (baseNote == null) return mutableSetOf()
val mySet = mutableSetOf<User>()
addFollowsThatDirectlyParticipateOnToSet(baseNote, followingSet, mySet)
baseNote.replies.forEach {
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
}
baseNote.boosts.forEach {
it.replyTo?.forEach {
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
}
}
LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.values?.forEach {
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
}
return mySet
}
fun countFollowsThatParticipateOn(baseNote: Note?, followingSet: Set<HexKey>?): Int {
if (baseNote == null) return 0
val list = followsThatParticipateOn(baseNote, followingSet)
return list.size
}
}

View File

@@ -0,0 +1,250 @@
package com.vitorpamplona.amethyst.model
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class RelayInformation(
val name: String?,
val description: String?,
val pubkey: String?,
val contact: String?,
val supported_nips: List<Int>?,
val supported_nip_extensions: List<String>?,
val software: String?,
val version: String?,
val limitation: RelayInformationLimitation?,
val relay_countries: List<String>?,
val language_tags: List<String>?,
val tags: List<String>?,
val posting_policy: String?,
val payments_url: String?,
val fees: RelayInformationFees?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformation::class.java, RelayInformationSerializer())
.registerTypeAdapter(RelayInformationLimitation::class.java, RelayInformationLimitationSerializer())
.registerTypeAdapter(RelayInformationFees::class.java, RelayInformationFeesSerializer())
.registerTypeAdapter(RelayInformationFee::class.java, RelayInformationFeeSerializer())
.create()
fun fromJson(json: String): RelayInformation = gson.fromJson(json, RelayInformation::class.java)
}
}
class RelayInformationFee(
val amount: Int?,
val unit: String?,
val period: Int?,
val kinds: List<Int>?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformationFee::class.java, RelayInformationFeeSerializer())
.create()
fun fromJson(json: String): RelayInformationFee = gson.fromJson(json, RelayInformationFee::class.java)
}
}
private class RelayInformationFeeSerializer : JsonSerializer<RelayInformationFee> {
override fun serialize(
src: RelayInformationFee,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("amount", src.amount)
addProperty("unit", src.unit)
addProperty("period", src.period)
add(
"kinds",
JsonArray().also { kinds ->
src.kinds?.forEach { kind ->
kinds.add(
kind
)
}
}
)
}
}
}
class RelayInformationFees(
val admission: List<RelayInformationFee>?,
val subscription: List<RelayInformationFee>?,
val publication: List<RelayInformationFee>?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformationFees::class.java, RelayInformationFeesSerializer())
.create()
fun fromJson(json: String): RelayInformationFees = gson.fromJson(json, RelayInformationFees::class.java)
}
}
private class RelayInformationFeesSerializer : JsonSerializer<RelayInformationFees> {
override fun serialize(
src: RelayInformationFees,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
add(
"admission",
JsonArray().also { admissions ->
src.admission?.forEach { admission ->
admissions.add(
admission.toString()
)
}
}
)
add(
"publication",
JsonArray().also { publications ->
src.publication?.forEach { publication ->
publications.add(
publication.toString()
)
}
}
)
add(
"subscription",
JsonArray().also { subscriptions ->
src.subscription?.forEach { subscription ->
subscriptions.add(
subscription.toString()
)
}
}
)
}
}
}
class RelayInformationLimitation(
val max_message_length: Int?,
val max_subscriptions: Int?,
val max_filters: Int?,
val max_limit: Int?,
val max_subid_length: Int?,
val min_prefix: Int?,
val max_event_tags: Int?,
val max_content_length: Int?,
val min_pow_difficulty: Int?,
val auth_required: Boolean?,
val payment_required: Boolean?
) {
companion object {
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(RelayInformationLimitation::class.java, RelayInformationLimitationSerializer())
.create()
fun fromJson(json: String): RelayInformationLimitation = gson.fromJson(json, RelayInformationLimitation::class.java)
}
}
private class RelayInformationLimitationSerializer : JsonSerializer<RelayInformationLimitation> {
override fun serialize(
src: RelayInformationLimitation,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("max_message_length", src.max_message_length)
addProperty("max_subscriptions", src.max_subscriptions)
addProperty("max_filters", src.max_filters)
addProperty("max_limit", src.max_limit)
addProperty("max_subid_length", src.max_subid_length)
addProperty("min_prefix", src.min_prefix)
addProperty("max_event_tags", src.max_event_tags)
addProperty("max_content_length", src.max_content_length)
addProperty("min_pow_difficulty", src.min_pow_difficulty)
addProperty("auth_required", src.auth_required)
addProperty("payment_required", src.payment_required)
}
}
}
private class RelayInformationSerializer : JsonSerializer<RelayInformation> {
override fun serialize(
src: RelayInformation,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("name", src.name)
addProperty("description", src.description)
addProperty("pubkey", src.pubkey)
addProperty("contact", src.contact)
add(
"supported_nip_extensions",
JsonArray().also { supported_nip_extensions ->
src.supported_nip_extensions?.forEach { nip ->
supported_nip_extensions.add(
nip
)
}
}
)
add(
"supported_nips",
JsonArray().also { supported_nips ->
src.supported_nips?.forEach { nip ->
supported_nips.add(
nip
)
}
}
)
addProperty("software", src.software)
addProperty("version", src.version)
add(
"relay_countries",
JsonArray().also { relay_countries ->
src.relay_countries?.forEach { country ->
relay_countries.add(
country
)
}
}
)
add(
"language_tags",
JsonArray().also { language_tags ->
src.language_tags?.forEach { language_tag ->
language_tags.add(
language_tag
)
}
}
)
add(
"tags",
JsonArray().also { tags ->
src.tags?.forEach { tag ->
tags.add(
tag
)
}
}
)
addProperty("posting_policy", src.posting_policy)
addProperty("payments_url", src.payments_url)
}
}
}

View File

@@ -1,14 +1,17 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.service.relays.FeedType
@Immutable
data class RelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val errorCount: Int = 0,
val downloadCount: Int = 0,
val uploadCount: Int = 0,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val feedTypes: Set<FeedType>
val feedTypes: Set<FeedType>,
val paidRelay: Boolean = false
)

View File

@@ -0,0 +1,11 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
@Stable
class Settings(
var automaticallyShowImages: Boolean? = null,
var automaticallyStartPlayback: Boolean? = null,
var preferredLanguage: String? = null,
var automaticallyShowUrlPreview: Boolean? = null
)

View File

@@ -1,14 +1,18 @@
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
class ThreadAssembler {
fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
private fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
if (note.event is RepostEvent || note.event is GenericRepostEvent) return note
testedNotes.add(note)
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
@@ -35,17 +39,10 @@ class ThreadAssembler {
@OptIn(ExperimentalTime::class)
fun findThreadFor(noteId: String): Set<Note> {
checkNotInMainThread()
val (result, elapsed) = measureTimedValue {
val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId, null)
if (aTag != null) {
LocalCache.getOrCreateAddressableNote(aTag)
} else {
return emptySet()
}
} else {
LocalCache.getOrCreateNote(noteId)
}
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return emptySet<Note>()
if (note.event != null) {
val thread = mutableSetOf<Note>()
@@ -53,6 +50,11 @@ class ThreadAssembler {
val threadRoot = searchRoot(note, thread) ?: note
loadDown(threadRoot, thread)
// adds the replies of the note in case the search for Root
// did not added them.
note.replies.forEach {
loadDown(it, thread)
}
thread.toSet()
} else {

View File

@@ -0,0 +1,13 @@
package com.vitorpamplona.amethyst.model
object TimeUtils {
const val fiveMinutes = 60 * 5
const val oneHour = 60 * 60
const val oneDay = 24 * 60 * 60
fun now() = System.currentTimeMillis() / 1000
fun fiveMinutesAgo() = now() - fiveMinutes
fun oneHourAgo() = now() - oneHour
fun oneDayAgo() = now() - oneDay
fun eightHoursAgo() = now() - (oneHour * 8)
}

View File

@@ -1,75 +1,58 @@
package com.vitorpamplona.amethyst.model
import com.baha.url.preview.BahaUrlPreview
import com.baha.url.preview.IUrlPreviewCallback
import com.baha.url.preview.UrlInfoItem
import com.vitorpamplona.amethyst.ui.components.imageExtension
import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.videoExtension
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
@Stable
object UrlCachedPreviewer {
var cache = mapOf<String, UrlInfoItem>()
private set
var failures = mapOf<String, Throwable>()
var cache = LruCache<String, UrlPreviewState>(100)
private set
fun previewInfo(url: String, callback: IUrlPreviewCallback? = null) {
fun previewInfo(url: String, onReady: (UrlPreviewState) -> Unit) {
checkNotInMainThread()
cache[url]?.let {
callback?.onComplete(it)
onReady(it)
return
}
failures[url]?.let {
callback?.onFailed(it)
return
}
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
BahaUrlPreview(
url,
object : IUrlPreviewCallback {
override fun onComplete(urlInfo: UrlInfoItem) {
cache = cache + Pair(url, urlInfo)
callback?.onComplete(urlInfo)
BahaUrlPreview(
url,
object : IUrlPreviewCallback {
override fun onComplete(urlInfo: UrlInfoItem) {
cache[url]?.let {
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
onReady(it)
return
}
}
override fun onFailed(throwable: Throwable) {
failures = failures + Pair(url, throwable)
callback?.onFailed(throwable)
val state = if (urlInfo.allFetchComplete() && urlInfo.url == url) {
UrlPreviewState.Loaded(urlInfo)
} else {
UrlPreviewState.Empty
}
cache.put(url, state)
onReady(state)
}
).fetchUrlPreview()
}
}
fun findUrlsInMessage(message: String): List<String> {
return message.split('\n').map { paragraph ->
paragraph.split(' ').filter { word: String ->
isValidURL(word) || noProtocolUrlValidator.matcher(word).matches()
}
}.flatten()
}
override fun onFailed(throwable: Throwable) {
cache[url]?.let {
onReady(it)
return
}
fun preloadPreviewsFor(note: Note) {
note.event?.content()?.let {
findUrlsInMessage(it).forEach {
val removedParamsFromUrl = it.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
// Preload Images? Isn't this too heavy?
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
// Do nothing for now.
} else if (isValidURL(removedParamsFromUrl)) {
previewInfo(it)
} else {
previewInfo("https://$it")
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
cache.put(url, state)
onReady(state)
}
}
}
).fetchUrlPreview()
}
}

View File

@@ -1,41 +1,41 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nostr.postr.Bech32
import nostr.postr.toNpub
import java.math.BigDecimal
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
@Stable
class User(val pubkeyHex: String) {
var info: UserMetadata? = null
var latestContactList: ContactListEvent? = null
var notes = setOf<Note>()
private set
var latestBookmarkList: BookmarkListEvent? = null
var reports = mapOf<User, Set<Note>>()
private set
var latestReportTime: Long = 0
var latestEOSEs: Map<String, EOSETime> = emptyMap()
var zaps = mapOf<Note, Note?>()
private set
@@ -46,12 +46,13 @@ class User(val pubkeyHex: String) {
var privateChatrooms = mapOf<User, Chatroom>()
private set
var acceptedBadges: AddressableNote? = null
fun pubkey() = Hex.decode(pubkeyHex)
fun pubkeyNpub() = pubkey().toNpub()
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
fun toNostrUri() = "nostr:${pubkeyNpub()}"
override fun toString(): String = pubkeyHex
fun toBestDisplayName(): String {
@@ -75,6 +76,13 @@ class User(val pubkeyHex: String) {
return info?.picture
}
fun updateBookmark(event: BookmarkListEvent) {
if (event.id == latestBookmarkList?.id) return
latestBookmarkList = event
liveSet?.bookmarks?.invalidateData()
}
fun updateContactList(event: ContactListEvent) {
if (event.id == latestContactList?.id) return
@@ -87,30 +95,15 @@ class User(val pubkeyHex: String) {
// Update Followers of the past user list
// Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.follows?.invalidateData()
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
}
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.follows?.invalidateData()
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
}
liveSet?.relays?.invalidateData()
}
fun addNote(note: Note) {
if (note !in notes) {
notes = notes + note
// No need for Listener yet
}
}
fun removeNote(note: Note) {
notes = notes - note
}
fun clearNotes() {
notes = setOf<Note>()
}
fun addReport(note: Note) {
val author = note.author ?: return
@@ -121,11 +114,6 @@ class User(val pubkeyHex: String) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveSet?.reports?.invalidateData()
}
val reportTime = note.createdAt() ?: 0
if (reportTime > latestReportTime) {
latestReportTime = reportTime
}
}
fun removeReport(deleteNote: Note) {
@@ -139,11 +127,6 @@ class User(val pubkeyHex: String) {
}
}
fun updateAcceptedBadges(note: AddressableNote) {
acceptedBadges = note
liveSet?.badges?.invalidateData()
}
fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
@@ -181,18 +164,34 @@ class User(val pubkeyHex: String) {
}
@Synchronized
fun getOrCreatePrivateChatroom(user: User): Chatroom {
private fun getOrCreatePrivateChatroomSync(user: User): Chatroom {
checkNotInMainThread()
return privateChatrooms[user] ?: run {
val privateChatroom = Chatroom(setOf<Note>())
val privateChatroom = Chatroom()
privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
privateChatroom
}
}
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
return privateChatrooms[user] ?: getOrCreatePrivateChatroomSync(user)
}
fun addMessage(user: User, msg: Note) {
val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg !in privateChatroom.roomMessages) {
privateChatroom.roomMessages = privateChatroom.roomMessages + msg
privateChatroom.addMessageSync(msg)
liveSet?.messages?.invalidateData()
}
}
fun removeMessage(user: User, msg: Note) {
checkNotInMainThread()
val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg in privateChatroom.roomMessages) {
privateChatroom.removeMessageSync(msg)
liveSet?.messages?.invalidateData()
}
}
@@ -215,6 +214,7 @@ class User(val pubkeyHex: String) {
info = newUserInfo
info?.latestMetadata = latestMetadata
info?.updatedMetadataAt = latestMetadata.createdAt
info?.tags = latestMetadata.tags.toImmutableListOfLists()
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
try {
@@ -236,35 +236,63 @@ class User(val pubkeyHex: String) {
}
fun isFollowing(user: User): Boolean {
return (latestContactList)?.unverifiedFollowKeySet()?.toSet()?.let {
return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
}
fun isFollowingHashtag(tag: String): Boolean {
return latestContactList?.isTaggedHash(tag) ?: false
}
fun isFollowingHashtagCached(tag: String): Boolean {
return latestContactList?.verifiedFollowTagSet?.let {
return tag.lowercase() in it
} ?: false
}
fun isFollowingCached(user: User): Boolean {
return latestContactList?.verifiedFollowKeySet?.let {
return user.pubkeyHex in it
} ?: false
}
fun isFollowingCached(userHex: String): Boolean {
return latestContactList?.verifiedFollowKeySet?.let {
return userHex in it
} ?: false
}
fun transientFollowCount(): Int? {
return latestContactList?.unverifiedFollowKeySet()?.size
}
fun transientFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false }
suspend fun transientFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
fun cachedFollowingKeySet(): Set<HexKey> {
return latestContactList?.verifiedFollowKeySet ?: emptySet()
}
fun cachedFollowingTagSet(): Set<HexKey> {
return latestContactList?.verifiedFollowTagSet ?: emptySet()
}
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
}
fun cachedFollowCount(): Int? {
return latestContactList?.verifiedFollowKeySet?.size
}
fun cachedFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false }
suspend fun cachedFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
fun hasSentMessagesTo(user: User?): Boolean {
val messagesToUser = privateChatrooms[user] ?: return false
return messagesToUser.roomMessages.any { this == it.author }
return messagesToUser.roomMessages.any { this.pubkeyHex == it.author?.pubkeyHex }
}
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
@@ -296,34 +324,58 @@ class User(val pubkeyHex: String) {
class UserLiveSet(u: User) {
// UI Observers line up here.
val follows: UserLiveData = UserLiveData(u)
val followers: UserLiveData = UserLiveData(u)
val reports: UserLiveData = UserLiveData(u)
val messages: UserLiveData = UserLiveData(u)
val relays: UserLiveData = UserLiveData(u)
val relayInfo: UserLiveData = UserLiveData(u)
val metadata: UserLiveData = UserLiveData(u)
val zaps: UserLiveData = UserLiveData(u)
val badges: UserLiveData = UserLiveData(u)
val bookmarks: UserLiveData = UserLiveData(u)
fun isInUse(): Boolean {
return follows.hasObservers() ||
followers.hasObservers() ||
reports.hasObservers() ||
messages.hasObservers() ||
relays.hasObservers() ||
relayInfo.hasObservers() ||
metadata.hasObservers() ||
zaps.hasObservers() ||
badges.hasObservers()
bookmarks.hasObservers()
}
}
@Immutable
data class RelayInfo(
val url: String,
var lastEvent: Long,
var counter: Long
)
data class Chatroom(var roomMessages: Set<Note>)
class Chatroom() {
var roomMessages: Set<Note> = setOf()
@Synchronized
fun addMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
}
}
@Synchronized
fun removeMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
}
}
}
@Stable
class UserMetadata {
var name: String? = null
var username: String? = null
@@ -349,38 +401,55 @@ class UserMetadata {
var updatedMetadataAt: Long = 0
var latestMetadata: MetadataEvent? = null
var tags: ImmutableListOfLists<String>? = null
fun anyName(): String? {
return display_name ?: displayName ?: name ?: username
}
fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
.any { it.startsWith(prefix, true) }
.any { it.contains(prefix, true) }
}
fun lnAddress(): String? {
return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null }
}
fun bestUsername(): String? {
return name?.ifBlank { null } ?: username?.ifBlank { null }
}
fun bestDisplayName(): String? {
return displayName?.ifBlank { null } ?: display_name?.ifBlank { null }
}
fun nip05(): String? {
return nip05?.ifBlank { null }
}
fun profilePicture(): String? {
if (picture.isNullOrBlank()) picture = null
return picture
}
}
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
// Refreshes observers in batches.
var handlerWaiting = AtomicBoolean()
private val bundler = BundledUpdate(500, Dispatchers.IO)
fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return
checkNotInMainThread()
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
try {
delay(100)
refresh()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
bundler.invalidate() {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(UserState(user))
}
}
}
private fun refresh() {
postValue(UserState(user))
}
override fun onActive() {
super.onActive()
NostrSingleUserDataSource.add(user)
@@ -392,4 +461,5 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
}
}
@Immutable
class UserState(val user: User)

View File

@@ -0,0 +1,205 @@
package com.vitorpamplona.amethyst.service
import android.graphics.Bitmap
import android.graphics.Color
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
object BlurHashDecoder {
// cache Math.cos() calculations to improve performance.
// The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps
// the cache is enabled by default, it is recommended to disable it only when just a few images are displayed
private val cacheCosinesX = HashMap<Int, DoubleArray>()
private val cacheCosinesY = HashMap<Int, DoubleArray>()
/**
* Clear calculations stored in memory cache.
* The cache is not big, but will increase when many image sizes are used,
* if the app needs memory it is recommended to clear it.
*/
fun clearCache() {
cacheCosinesX.clear()
cacheCosinesY.clear()
}
/**
* Returns width/height
*/
fun aspectRatio(blurHash: String?): Float? {
if (blurHash == null || blurHash.length < 6) {
return null
}
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
return numCompX.toFloat() / numCompY.toFloat()
}
/**
* Decode a blur hash into a new bitmap.
*
* @param useCache use in memory cache for the calculated math, reused by images with same size.
* if the cache does not exist yet it will be created and populated with new calculations.
* By default it is true.
*/
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? {
checkNotInMainThread()
if (blurHash == null || blurHash.length < 6) {
return null
}
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors, useCache)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
}
private fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int,
height: Int,
numCompX: Int,
numCompY: Int,
colors: Array<FloatArray>,
useCache: Boolean
): Bitmap {
// use an array for better performance when writing pixel colors
val imageArray = IntArray(width * height)
val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)
val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)
val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)
val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width)
val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height)
val basis = (cosX * cosY).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
}
}
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
}
private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when {
calculate -> {
DoubleArray(height * numCompY).also {
cacheCosinesY[height * numCompY] = it
}
}
else -> {
cacheCosinesY[height * numCompY]!!
}
}
private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when {
calculate -> {
DoubleArray(width * numCompX).also {
cacheCosinesX[width * numCompX] = it
}
}
else -> cacheCosinesX[width * numCompX]!!
}
private fun DoubleArray.getCos(
calculate: Boolean,
x: Int,
numComp: Int,
y: Int,
size: Int
): Double {
if (calculate) {
this[x + numComp * y] = cos(Math.PI * y * x / size)
}
return this[x + numComp * y]
}
private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View File

@@ -0,0 +1,68 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DataSource
import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.request.ImageRequest
import coil.request.Options
import java.net.URLDecoder
import java.net.URLEncoder
import kotlin.math.roundToInt
@Stable
class BlurHashFetcher(
private val options: Options,
private val data: Uri
) : Fetcher {
override suspend fun fetch(): FetchResult {
checkNotInMainThread()
val encodedHash = data.toString().removePrefix("bluehash:")
val hash = URLDecoder.decode(encodedHash, "utf-8")
val aspectRatio = BlurHashDecoder.aspectRatio(hash) ?: 1.0f
val preferredWidth = 100
val bitmap = BlurHashDecoder.decode(
hash,
preferredWidth,
(preferredWidth * (1 / aspectRatio)).roundToInt()
)
if (bitmap == null) {
throw Exception("Unable to convert Bluehash $hash")
}
return DrawableResult(
drawable = bitmap.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.MEMORY
)
}
object Factory : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher {
return BlurHashFetcher(options, data)
}
}
}
object BlurHashRequester {
fun imageRequest(context: Context, message: String): ImageRequest {
val encodedMessage = URLEncoder.encode(message, "utf-8")
return ImageRequest
.Builder(context)
.data("bluehash:$encodedMessage")
.fetcherFactory(BlurHashFetcher.Factory)
.build()
}
}

View File

@@ -0,0 +1,291 @@
package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import android.util.Patterns
import androidx.compose.runtime.Immutable
import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlContent
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.startsWithNIP19Scheme
import com.vitorpamplona.amethyst.ui.components.tagIndex
import com.vitorpamplona.amethyst.ui.components.videoExtensions
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import java.util.regex.Pattern
@Immutable
data class RichTextViewerState(
val urlSet: ImmutableSet<String>,
val imagesForPager: ImmutableMap<String, ZoomableUrlContent>,
val imageList: ImmutableList<ZoomableUrlContent>,
val customEmoji: ImmutableMap<String, String>,
val paragraphs: ImmutableList<ParagraphState>
)
data class ParagraphState(val words: ImmutableList<Segment>, val isRTL: Boolean)
object CachedRichTextParser {
val richTextCache = LruCache<String, RichTextViewerState>(200)
fun parseText(content: String, tags: ImmutableListOfLists<String>): RichTextViewerState {
return if (richTextCache[content] != null) {
richTextCache[content]
} else {
val newUrls = RichTextParser().parseText(content, tags)
richTextCache.put(content, newUrls)
newUrls
}
}
}
// Group 1 = url, group 4 additional chars
// val noProtocolUrlValidator = Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
// Android9 seems to have an issue starting this regex.
val noProtocolUrlValidator = try {
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]*[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)")
} catch (e: Exception) {
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
}
class RichTextParser() {
fun parseText(
content: String,
tags: ImmutableListOfLists<String>
): RichTextViewerState {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet = urls.mapNotNullTo(LinkedHashSet(urls.size)) {
// removes e-mails
if (Patterns.EMAIL_ADDRESS.matcher(it.originalUrl).matches()) {
null
} else if (isNumber(it.originalUrl)) {
null
} else if (it.originalUrl.contains("")) {
null
} else {
val pattern = "^((http|https)://)?([A-Za-z0-9-]+(\\.[A-Za-z0-9]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE)
if (pattern.matches(it.originalUrl)) {
it.originalUrl
} else {
null
}
}
}
val imagesForPager = urlSet.mapNotNull { fullUrl ->
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlImage(fullUrl)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlVideo(fullUrl)
} else {
null
}
}.associateBy { it.url }
val imageList = imagesForPager.values.toList()
val emojiMap =
tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags)
return RichTextViewerState(
urlSet.toImmutableSet(),
imagesForPager.toImmutableMap(),
imageList.toImmutableList(),
emojiMap.toImmutableMap(),
segments
)
}
private fun findTextSegments(content: String, images: Set<String>, urls: Set<String>, emojis: Map<String, String>, tags: ImmutableListOfLists<String>): ImmutableList<ParagraphState> {
var paragraphSegments = persistentListOf<ParagraphState>()
content.split('\n').forEach { paragraph ->
var segments = persistentListOf<Segment>()
var isDirty = false
val isRTL = isArabic(paragraph)
val wordList = paragraph.split(' ')
wordList.forEach { word ->
val wordSegment = wordIdentifier(word, images, urls, emojis, tags)
if (wordSegment !is RegularTextSegment) {
isDirty = true
}
segments = segments.add(wordSegment)
}
val newSegments = if (isDirty) {
ParagraphState(segments, isRTL)
} else {
ParagraphState(persistentListOf<Segment>(RegularTextSegment(paragraph)), isRTL)
}
paragraphSegments = paragraphSegments.add(newSegments)
}
return paragraphSegments
}
fun isNumber(word: String): Boolean {
return numberPattern.matcher(word).matches()
}
fun isDate(word: String): Boolean {
return shortDatePattern.matcher(word).matches() || longDatePattern.matcher(word).matches()
}
private fun isArabic(text: String): Boolean {
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
}
private fun wordIdentifier(word: String, images: Set<String>, urls: Set<String>, emojis: Map<String, String>, tags: ImmutableListOfLists<String>): Segment {
val emailMatcher = Patterns.EMAIL_ADDRESS.matcher(word)
val phoneMatcher = Patterns.PHONE.matcher(word)
val schemelessMatcher = noProtocolUrlValidator.matcher(word)
return if (word.isEmpty()) {
RegularTextSegment(word)
} else if (images.contains(word)) {
ImageSegment(word)
} else if (urls.contains(word)) {
LinkSegment(word)
} else if (emojis.any { word.contains(it.key) }) {
EmojiSegment(word)
} else if (word.startsWith("lnbc", true)) {
InvoiceSegment(word)
} else if (word.startsWith("lnurl", true)) {
WithdrawSegment(word)
} else if (word.startsWith("cashuA", true)) {
CashuSegment(word)
} else if (emailMatcher.matches()) {
EmailSegment(word)
} else if (word.length in 7..14 && !isDate(word) && phoneMatcher.matches()) {
PhoneSegment(word)
} else if (startsWithNIP19Scheme(word)) {
BechSegment(word)
} else if (word.startsWith("#")) {
parseHash(word, tags)
} else if (schemelessMatcher.find()) {
val url = schemelessMatcher.group(1) // url
val additionalChars = schemelessMatcher.group(4) // additional chars
val pattern = "^([A-Za-z0-9-]+(\\.[A-Za-z0-9]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE)
if (pattern.matches(word)) {
SchemelessUrlSegment(word, url, additionalChars)
} else {
RegularTextSegment(word)
}
} else {
RegularTextSegment(word)
}
}
private fun parseHash(word: String, tags: ImmutableListOfLists<String>): Segment {
// First #[n]
val matcher = tagIndex.matcher(word)
try {
if (matcher.find()) {
val index = matcher.group(1)?.toInt()
val suffix = matcher.group(2)
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
return HashIndexUserSegment(word, tag[1], suffix)
} else if (tag[0] == "e" || tag[0] == "a") {
return HashIndexEventSegment(word, tag[1], suffix)
}
}
}
}
} catch (e: Exception) {
Log.w("Tag Parser", "Couldn't link tag $word", e)
}
// Second #Amethyst
val hashtagMatcher = hashTagsPattern.matcher(word)
try {
if (hashtagMatcher.find()) {
val hashtag = hashtagMatcher.group(1)
if (hashtag != null) {
return HashTagSegment(word, hashtag, hashtagMatcher.group(2))
}
}
} catch (e: Exception) {
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
}
return RegularTextSegment(word)
}
companion object {
val longDatePattern: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$")
val shortDatePattern: Pattern = Pattern.compile("^\\d{2}-\\d{2}-\\d{2}$")
val numberPattern: Pattern = Pattern.compile("^(-?[\\d.]+)([a-zA-Z%]*)$")
}
}
@Immutable
open class Segment(val segmentText: String)
@Immutable
class ImageSegment(segment: String) : Segment(segment)
@Immutable
class LinkSegment(segment: String) : Segment(segment)
@Immutable
class EmojiSegment(segment: String) : Segment(segment)
@Immutable
class InvoiceSegment(segment: String) : Segment(segment)
@Immutable
class WithdrawSegment(segment: String) : Segment(segment)
@Immutable
class CashuSegment(segment: String) : Segment(segment)
@Immutable
class EmailSegment(segment: String) : Segment(segment)
@Immutable
class PhoneSegment(segment: String) : Segment(segment)
@Immutable
class BechSegment(segment: String) : Segment(segment)
@Immutable
open class HashIndexSegment(segment: String, val hex: String, val extras: String?) : Segment(segment)
@Immutable
class HashIndexUserSegment(segment: String, hex: String, extras: String?) : HashIndexSegment(segment, hex, extras)
@Immutable
class HashIndexEventSegment(segment: String, hex: String, extras: String?) : HashIndexSegment(segment, hex, extras)
@Immutable
class HashTagSegment(segment: String, val hashtag: String, val extras: String?) : Segment(segment)
@Immutable
class SchemelessUrlSegment(segment: String, val url: String, val extras: String?) : Segment(segment)
@Immutable
class RegularTextSegment(segment: String) : Segment(segment)

View File

@@ -0,0 +1,98 @@
package com.vitorpamplona.amethyst.service
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Base64
@Immutable
data class CashuToken(
val token: String,
val mint: String,
val totalAmount: Long,
val fees: Int,
val redeemInvoiceAmount: Long,
val proofs: JsonArray
)
class CashuProcessor {
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
checkNotInMainThread()
try {
val base64token = cashuToken.replace("cashuA", "")
val cashu = JsonParser.parseString(String(Base64.getDecoder().decode(base64token)))
val token = cashu.asJsonObject.get("token").asJsonArray[0].asJsonObject
val proofs = token["proofs"].asJsonArray
val mint = token["mint"].asString
var totalAmount = 0L
for (proof in proofs) {
totalAmount += proof.asJsonObject["amount"].asLong
}
val fees = Math.max(((totalAmount * 0.02).toInt()), 2)
val redeemInvoiceAmount = totalAmount - fees
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs))
} catch (e: Exception) {
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
}
}
fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
checkNotInMainThread()
runCatching {
LightningAddressResolver().lnAddressInvoice(
lnaddress = lud16,
milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees
message = "Redeem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, onSuccess, onError)
},
onProgress = {
},
onError = onError
)
}
}
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
try {
val client = HttpClient.getHttpClient()
val url = token.mint + "/melt" // Melt cashu tokens at Mint
val jsonObject = JsonObject()
jsonObject.add("proofs", token.proofs)
jsonObject.addProperty("pr", invoice)
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request = Request.Builder()
.url(url)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val body = response.body.string()
val tree = jacksonObjectMapper().readTree(body)
val successful = tree?.get("paid")?.asText() == "true"
if (successful) {
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
} else {
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.")
}
} catch (e: Exception) {
onError("Token melt failure: " + e.message)
}
}
}

View File

@@ -0,0 +1,91 @@
package com.vitorpamplona.amethyst.service
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
import io.trbl.blurhash.BlurHash
import java.security.MessageDigest
import kotlin.math.roundToInt
class FileHeader(
val url: String,
val mimeType: String?,
val hash: String,
val size: Int,
val dim: String?,
val blurHash: String?,
val description: String? = null,
val sensitiveContent: Boolean = false
) {
companion object {
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
try {
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
if (imageData != null) {
prepare(imageData, fileUrl, mimeType, description, sensitiveContent, onReady, onError)
} else {
onError()
}
} catch (e: Exception) {
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
onError()
}
}
fun prepare(
data: ByteArray,
fileUrl: String,
mimeType: String?,
description: String?,
sensitiveContent: Boolean,
onReady: (FileHeader) -> Unit,
onError: () -> Unit
) {
try {
val sha256 = MessageDigest.getInstance("SHA-256")
val hash = sha256.digest(data).toHexKey()
val size = data.size
val (blurHash, dim) = if (mimeType?.startsWith("image/") == true) {
val opt = BitmapFactory.Options()
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
val intArray = IntArray(mBitmap.width * mBitmap.height)
mBitmap.getPixels(
intArray,
0,
mBitmap.width,
0,
0,
mBitmap.width,
mBitmap.height
)
val dim = "${mBitmap.width}x${mBitmap.height}"
val aspectRatio = (mBitmap.width).toFloat() / (mBitmap.height).toFloat()
if (aspectRatio > 1) {
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 9, (9 * (1 / aspectRatio)).roundToInt()), dim)
} else if (aspectRatio < 1) {
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, (9 * aspectRatio).roundToInt(), 9), dim)
} else {
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
}
} else {
Pair(null, null)
}
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description, sensitiveContent))
} catch (e: Exception) {
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
onError()
}
}
}
}

View File

@@ -0,0 +1,23 @@
package com.vitorpamplona.amethyst.service
object HexValidator {
private fun isHex2(c: Char): Boolean {
return when (c) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', ' ' -> true
else -> false
}
}
fun isHex(hex: String?): Boolean {
if (hex == null) return false
var isHex = true
for (c in hex.toCharArray()) {
if (!isHex2(c)) {
isHex = false
break
}
}
return isHex
}
}

View File

@@ -0,0 +1,44 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
import kotlin.properties.Delegates
object HttpClient {
var proxyChangeListeners = ArrayList<() -> Unit>()
// fires off every time value of the property changes
private var internalProxy: Proxy? by Delegates.observable(null) { _, oldValue, newValue ->
if (oldValue != newValue) {
proxyChangeListeners.forEach {
it()
}
}
}
fun start(account: Account?) {
this.internalProxy = account?.proxy
}
fun getHttpClient(): OkHttpClient {
val seconds = if (internalProxy != null) 20L else 10L
val duration = Duration.ofSeconds(seconds)
return OkHttpClient.Builder()
.proxy(internalProxy)
.readTimeout(duration)
.connectTimeout(duration)
.writeTimeout(duration)
.build()
}
fun getProxy(): Proxy? {
return internalProxy
}
fun initProxy(useProxy: Boolean, hostname: String, port: Int): Proxy? {
return if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port)) else null
}
}

View File

@@ -0,0 +1,12 @@
package com.vitorpamplona.amethyst.service
import android.os.Looper
import com.vitorpamplona.amethyst.BuildConfig
fun checkNotInMainThread() {
if (BuildConfig.DEBUG && isMainThread()) throw OnMainThreadException("It should not be in the MainThread")
}
fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
class OnMainThreadException(str: String) : RuntimeException(str)

View File

@@ -0,0 +1,34 @@
package com.vitorpamplona.amethyst.service
import androidx.compose.runtime.Immutable
import java.util.regex.Pattern
@Immutable
class NIP30Parser {
val customEmojiPattern: Pattern = Pattern.compile("\\:([A-Za-z0-9_\\-]+)\\:", Pattern.CASE_INSENSITIVE)
fun buildArray(input: String): List<String> {
val matcher = customEmojiPattern.matcher(input)
val list = mutableListOf<String>()
while (matcher.find()) {
list.add(matcher.group())
}
if (list.isEmpty()) {
return listOf(input)
}
val regularChars = input.split(customEmojiPattern.toRegex())
val finalList = mutableListOf<String>()
var index = 0
for (e in regularChars) {
finalList.add(e)
if (index < list.size) {
finalList.add(list[index])
}
index++
}
return finalList
}
}

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.service
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -8,19 +9,19 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
class Nip05Verifier {
val client = OkHttpClient.Builder().build()
class Nip05Verifier() {
fun assembleUrl(nip05address: String): String? {
val parts = nip05address.trim().split("@")
if (parts.size == 2) {
return "https://${parts[1]}/.well-known/nostr.json?name=${parts[0]}"
}
if (parts.size == 1) {
return "https://${parts[0]}/.well-known/nostr.json?name=_"
}
return null
}
@@ -33,6 +34,8 @@ class Nip05Verifier {
}
private suspend fun fetchNip05JsonSuspend(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
checkNotInMainThread()
val url = assembleUrl(nip05)
if (url == null) {
@@ -43,12 +46,14 @@ class Nip05Verifier {
withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.header("User-Agent", "Amethyst")
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
HttpClient.getHttpClient().newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
checkNotInMainThread()
response.use {
if (it.isSuccessful) {
onSuccess(it.body.string())
@@ -70,19 +75,32 @@ class Nip05Verifier {
}
fun verifyNip05(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
// check fails on tests
checkNotInMainThread()
val mapper = jacksonObjectMapper()
fetchNip05Json(
nip05,
onSuccess = {
checkNotInMainThread()
// NIP05 usernames are case insensitive, but JSON properties are not
// converts the json to lowercase and then tries to access the username via a
// lowercase version of the username.
val nip05url = try {
mapper.readTree(it)
mapper.readTree(it.lowercase())
} catch (t: Throwable) {
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
null
}
val user = nip05.split("@")[0]
val parts = nip05.split("@")
val user = if (parts.size == 2) {
parts[0].lowercase()
} else {
"_"
}
val hexKey = nip05url?.get("names")?.get(user)?.asText()

View File

@@ -0,0 +1,34 @@
package com.vitorpamplona.amethyst.ui.note
import android.net.Uri
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
// Rename to the corect nip number when ready.
object Nip47 {
fun parse(uri: String): Nip47URI {
// nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D
val url = Uri.parse(uri)
if (url.scheme != "nostrwalletconnect" && url.scheme != "nostr+walletconnect") {
throw IllegalArgumentException("Not a Wallet Connect QR Code")
}
val pubkey = url.host ?: throw IllegalArgumentException("Hostname cannot be null")
val pubkeyHex = try {
decodePublicKey(pubkey).toHexKey()
} catch (e: Exception) {
throw IllegalArgumentException("Hostname is not a valid Nostr Pubkey")
}
val relay = url.getQueryParameter("relay")
val secret = url.getQueryParameter("secret")
return Nip47URI(pubkeyHex, relay, secret)
}
}

View File

@@ -1,8 +1,10 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
@@ -11,16 +13,21 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrAccountDataSource : NostrDataSource("AccountData") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
fun createAccountContactListFilter(): TypedFilter {
return TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ContactListEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
@@ -31,7 +38,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
fun createAccountMetadataFilter(): TypedFilter {
return TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
@@ -42,43 +49,71 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
fun createAccountAcceptedAwardsFilter(): TypedFilter {
return TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind),
kinds = listOf(BadgeProfilesEvent.kind, EmojiPackSelectionEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1
limit = 10
)
)
}
fun createAccountBookmarkListFilter(): TypedFilter {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
limit = 100
)
)
}
fun createAccountReportsFilter(): TypedFilter {
return TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
authors = listOf(account.userProfile().pubkeyHex),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
)
)
}
fun createAccountLastPostsListFilter(): TypedFilter {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
authors = listOf(account.userProfile().pubkeyHex),
limit = 400
)
)
}
fun createNotificationFilter() = TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind,
PollNoteEvent.kind,
ReactionEvent.kind,
RepostEvent.kind,
GenericRepostEvent.kind,
ReportEvent.kind,
LnZapEvent.kind,
LnZapPaymentResponseEvent.kind,
ChannelMessageEvent.kind,
BadgeAwardEvent.kind
),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 200
limit = 4000,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
)
)
val accountChannel = requestNewChannel()
val accountChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
// gets everthing about the user logged in
@@ -87,7 +122,24 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountContactListFilter(),
createNotificationFilter(),
createAccountReportsFilter(),
createAccountAcceptedAwardsFilter()
createAccountAcceptedAwardsFilter(),
createAccountBookmarkListFilter(),
createAccountLastPostsListFilter()
).ifEmpty { null }
}
override fun auth(relay: Relay, challenge: String) {
super.auth(relay, challenge)
if (this::account.isInitialized) {
val event = account.createAuthEvent(relay, challenge)
if (event != null) {
Client.send(
event,
relay.url
)
}
}
}
}

View File

@@ -1,22 +1,63 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
var account: Account? = null
var channel: Channel? = null
fun loadMessagesBetween(channelId: String) {
channel = LocalCache.getOrCreateChannel(channelId)
fun loadMessagesBetween(account: Account, channel: Channel) {
this.account = account
this.channel = channel
resetFilters()
}
fun clear() {
account = null
channel = null
}
fun createMessagesByMeToChannelFilter(): TypedFilter? {
val myAccount = account ?: return null
if (channel is PublicChatChannel) {
// Brings on messages by the user from all other relays.
// Since we ship with write to public, read from private only
// this guarantees that messages from the author do not disappear.
return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter = JsonFilter(
kinds = listOf(ChannelMessageEvent.kind),
authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50
)
)
} else if (channel is LiveActivitiesChannel) {
// Brings on messages by the user from all other relays.
// Since we ship with write to public, read from private only
// this guarantees that messages from the author do not disappear.
return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter = JsonFilter(
kinds = listOf(LiveActivitiesChatMessageEvent.kind),
authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50
)
)
}
return null
}
fun createMessagesToChannelFilter(): TypedFilter? {
if (channel != null) {
if (channel is PublicChatChannel) {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
@@ -25,6 +66,15 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
limit = 200
)
)
} else if (channel is LiveActivitiesChannel) {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(LiveActivitiesChatMessageEvent.kind),
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
limit = 200
)
)
}
return null
}
@@ -32,6 +82,9 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
val messagesChannel = requestNewChannel()
override fun updateChannelFilters() {
messagesChannel.typedFilters = listOfNotNull(createMessagesToChannelFilter()).ifEmpty { null }
messagesChannel.typedFilters = listOfNotNull(
createMessagesToChannelFilter(),
createMessagesByMeToChannelFilter()
).ifEmpty { null }
}
}

View File

@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
@@ -12,9 +11,9 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
lateinit var account: Account
var withUser: User? = null
fun loadMessagesBetween(accountIn: Account, userId: String) {
fun loadMessagesBetween(accountIn: Account, user: User) {
account = accountIn
withUser = LocalCache.users[userId]
withUser = user
resetFilters()
}

View File

@@ -5,6 +5,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -12,11 +14,15 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
val chatRoomList = "ChatroomList"
fun createMessagesToMeFilter() = TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
)
)
@@ -24,7 +30,8 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
authors = listOf(account.userProfile().pubkeyHex),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
)
)
@@ -32,22 +39,30 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
authors = listOf(account.userProfile().pubkeyHex),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
)
)
fun createMyChannelsFilter() = TypedFilter(
types = FeedType.values().toSet(), // Metadata comes from any relay
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = account.followingChannels.toList()
)
)
fun createMyChannelsFilter(): TypedFilter {
val followingEvents = account.selectedChatsFollowList()
fun createLastChannelInfoFilter(): List<TypedFilter> {
return account.followingChannels.map {
return TypedFilter(
types = COMMON_FEED_TYPES, // Metadata comes from any relay
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = followingEvents.toList(),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
)
)
}
fun createLastChannelInfoFilter(): List<TypedFilter>? {
val followingEvents = account.selectedChatsFollowList()
return followingEvents.map {
TypedFilter(
types = FeedType.values().toSet(), // Metadata comes from any relay
types = COMMON_FEED_TYPES, // Metadata comes from any relay
filter = JsonFilter(
kinds = listOf(ChannelMetadataEvent.kind),
tags = mapOf("e" to listOf(it)),
@@ -57,20 +72,25 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
}
}
fun createLastMessageOfEachChannelFilter(): List<TypedFilter> {
return account.followingChannels.map {
fun createLastMessageOfEachChannelFilter(): List<TypedFilter>? {
val followingEvents = account.selectedChatsFollowList()
return followingEvents.map {
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(ChannelMessageEvent.kind),
tags = mapOf("e" to listOf(it)),
limit = 100 // Remember to consider spam that is being removed from the UI
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
limit = 50 // Remember to consider spam that is being removed from the UI
)
)
}
}
val chatroomListChannel = requestNewChannel()
val chatroomListChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), chatRoomList, relayUrl, time)
}
override fun updateChannelFilters() {
val list = listOf(

View File

@@ -0,0 +1,41 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") {
private var communityToWatch: AddressableNote? = null
private fun createLoadCommunityFilter(): TypedFilter? {
val myCommunityToWatch = communityToWatch ?: return null
val community = myCommunityToWatch.event as? CommunityDefinitionEvent ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
authors = community.moderators().map { it.key }.plus(listOfNotNull(myCommunityToWatch.author?.pubkeyHex)),
tags = mapOf(
"a" to listOf(myCommunityToWatch.address.toTag())
),
kinds = listOf(CommunityPostApprovalEvent.kind),
limit = 500
)
)
}
val loadCommunityChannel = requestNewChannel()
override fun updateChannelFilters() {
loadCommunityChannel.typedFilters = listOfNotNull(createLoadCommunityFilter()).ifEmpty { null }
}
fun loadCommunity(note: AddressableNote?) {
communityToWatch = note
invalidateFilters()
}
}

View File

@@ -2,40 +2,19 @@ package com.vitorpamplona.amethyst.service
import android.util.Log
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.DeletionEvent
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.Subscription
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.Error
abstract class NostrDataSource(val debugName: String) {
private var subscriptions = mapOf<String, Subscription>()
@@ -52,8 +31,6 @@ abstract class NostrDataSource(val debugName: String) {
private val clientListener = object : Client.Listener() {
override fun onEvent(event: Event, subscriptionId: String, relay: Relay) {
if (subscriptionId in subscriptions.keys) {
if (!event.hasValidSignature()) return
val key = "$debugName $subscriptionId ${event.kind}"
val keyValue = eventCounter.get(key)
if (keyValue != null) {
@@ -62,42 +39,7 @@ abstract class NostrDataSource(val debugName: String) {
eventCounter = eventCounter + Pair(key, Counter(1))
}
try {
when (event) {
is BadgeAwardEvent -> LocalCache.consume(event)
is BadgeDefinitionEvent -> LocalCache.consume(event)
is BadgeProfilesEvent -> LocalCache.consume(event)
is ChannelCreateEvent -> LocalCache.consume(event)
is ChannelHideMessageEvent -> LocalCache.consume(event)
is ChannelMessageEvent -> LocalCache.consume(event, relay)
is ChannelMetadataEvent -> LocalCache.consume(event)
is ChannelMuteUserEvent -> LocalCache.consume(event)
is ContactListEvent -> LocalCache.consume(event)
is DeletionEvent -> LocalCache.consume(event)
is LnZapEvent -> {
event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(event)
}
is LnZapRequestEvent -> LocalCache.consume(event)
is LongTextNoteEvent -> LocalCache.consume(event, relay)
is MetadataEvent -> LocalCache.consume(event)
is PrivateDmEvent -> LocalCache.consume(event, relay)
is ReactionEvent -> LocalCache.consume(event)
is RecommendRelayEvent -> LocalCache.consume(event)
is ReportEvent -> LocalCache.consume(event, relay)
is RepostEvent -> {
event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(event)
}
is TextNoteEvent -> LocalCache.consume(event, relay)
else -> {
Log.w("Event Not Supported", event.toJson())
}
}
} catch (e: Exception) {
e.printStackTrace()
}
LocalCache.verifyAndConsume(event, relay)
}
}
@@ -115,18 +57,28 @@ abstract class NostrDataSource(val debugName: String) {
if (type == Relay.Type.EOSE && channel != null) {
// updates a per subscripton since date
subscriptions[channel]?.updateEOSE(Date().time / 1000)
subscriptions[channel]?.updateEOSE(TimeUtils.now(), relay.url)
}
}
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {
}
override fun onAuth(relay: Relay, challenge: String) {
auth(relay, challenge)
}
}
init {
Log.d("Init", "${this.javaClass.simpleName} Subscribe")
Client.subscribe(clientListener)
}
fun destroy() {
stop()
Client.unsubscribe(clientListener)
}
open fun start() {
println("DataSource: ${this.javaClass.simpleName} Start")
resetFilters()
@@ -140,7 +92,7 @@ abstract class NostrDataSource(val debugName: String) {
}
}
fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Subscription {
fun requestNewChannel(onEOSE: ((Long, String) -> Unit)? = null): Subscription {
val newSubscription = Subscription(UUID.randomUUID().toString().substring(0, 4), onEOSE)
subscriptions = subscriptions + Pair(newSubscription.id, newSubscription)
return newSubscription
@@ -152,23 +104,15 @@ abstract class NostrDataSource(val debugName: String) {
}
// Refreshes observers in batches.
var handlerWaiting = AtomicBoolean()
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateFilters() {
if (handlerWaiting.getAndSet(true)) return
bundler.invalidate() {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
try {
delay(200)
resetFiltersSuspend()
} finally {
withContext(NonCancellable) {
handlerWaiting.set(false)
}
}
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
resetFiltersSuspend()
}
}
@@ -180,6 +124,8 @@ abstract class NostrDataSource(val debugName: String) {
}
fun resetFiltersSuspend() {
checkNotInMainThread()
// saves the channels that are currently active
val activeSubscriptions = subscriptions.values.filter { it.typedFilters != null }
// saves the current content to only update if it changes
@@ -219,4 +165,5 @@ abstract class NostrDataSource(val debugName: String) {
}
abstract fun updateChannelFilters()
open fun auth(relay: Relay, challenge: String) = Unit
}

View File

@@ -0,0 +1,149 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
fun createLiveStreamFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
val followKeys = follows?.map {
it.substring(0, 8)
}
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = followKeys,
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createPublicChatFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
val followKeys = follows?.map {
it.substring(0, 8)
}
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
authors = followKeys,
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createCommunitiesFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
val followKeys = follows?.map {
it.substring(0, 8)
}
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = followKeys,
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createLiveStreamTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
tags = mapOf(
"t" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createPublicChatsTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
tags = mapOf(
"t" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createCommunitiesTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
tags = mapOf(
"t" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
val discoveryFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters = listOfNotNull(
createLiveStreamFilter(),
createPublicChatFilter(),
createCommunitiesFilter(),
createLiveStreamTagsFilter(),
createPublicChatsTagsFilter(),
createCommunitiesTagsFilter()
).ifEmpty { null }
}
}

View File

@@ -1,24 +0,0 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrGlobalDataSource : NostrDataSource("GlobalFeed") {
fun createGlobalFilter() = TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind),
limit = 200
)
)
val globalFeedChannel = requestNewChannel()
override fun updateChannelFilters() {
globalFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null }
}
}

View File

@@ -0,0 +1,46 @@
package com.vitorpamplona.amethyst.service
import androidx.compose.ui.text.capitalize
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
private var hashtagToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = hashtagToWatch ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
tags = mapOf(
"t" to listOf(
hashToLoad,
hashToLoad.lowercase(),
hashToLoad.uppercase(),
hashToLoad.capitalize()
)
),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind),
limit = 200
)
)
}
val loadHashtagChannel = requestNewChannel()
override fun updateChannelFilters() {
loadHashtagChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
}
fun loadHashtag(tag: String?) {
hashtagToWatch = tag
invalidateFilters()
}
}

View File

@@ -2,8 +2,19 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -15,6 +26,8 @@ import kotlinx.coroutines.launch
object NostrHomeDataSource : NostrDataSource("HomeFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
private val cacheListener: (UserState) -> Unit = {
invalidateFilters()
}
@@ -40,27 +53,88 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
}
fun createFollowAccountsFilter(): TypedFilter {
val follows = account.followingKeySet()
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
val followKeys = follows.map {
val followKeys = follows?.map {
it.substring(0, 6)
}
val followSet = followKeys.plus(account.userProfile().pubkeyHex.substring(0, 6))
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 6))
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
kinds = listOf(
TextNoteEvent.kind,
RepostEvent.kind,
GenericRepostEvent.kind,
ClassifiedsEvent.kind,
LongTextNoteEvent.kind,
PollNoteEvent.kind,
HighlightEvent.kind,
AudioTrackEvent.kind,
PinListEvent.kind,
LiveActivitiesChatMessageEvent.kind,
LiveActivitiesEvent.kind
),
authors = followSet,
limit = 400
limit = 400,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
)
)
}
val followAccountChannel = requestNewChannel()
fun createFollowTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
tags = mapOf(
"t" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
)
)
}
fun createFollowCommunitiesFilter(): TypedFilter? {
val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
if (communitiesToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind,
LongTextNoteEvent.kind,
ClassifiedsEvent.kind,
HighlightEvent.kind,
AudioTrackEvent.kind,
PinListEvent.kind,
CommunityPostApprovalEvent.kind
),
tags = mapOf(
"a" to communitiesToLoad.toList()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
)
)
}
val followAccountChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
followAccountChannel.typedFilters = listOf(createFollowAccountsFilter()).ifEmpty { null }
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowCommunitiesFilter(), createFollowTagsFilter()).ifEmpty { null }
}
}

View File

@@ -0,0 +1,53 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.model.LnZapPaymentResponseEvent
import com.vitorpamplona.amethyst.service.model.RelayAuthEvent
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
class NostrLnZapPaymentResponseDataSource(
private val fromServiceHex: String,
private val toUserHex: String,
private val replyingToHex: String,
private val authSigningKey: ByteArray
) : NostrDataSource("LnZapPaymentResponseFeed") {
val feedTypes = setOf(FeedType.WALLET_CONNECT)
private fun createWalletConnectServiceWatcher(): TypedFilter {
// downloads all the reactions to a given event.
return TypedFilter(
types = feedTypes,
filter = JsonFilter(
kinds = listOf(LnZapPaymentResponseEvent.kind),
authors = listOf(fromServiceHex),
tags = mapOf(
"e" to listOf(replyingToHex),
"p" to listOf(toUserHex)
),
limit = 1
)
)
}
val channel = requestNewChannel()
override fun updateChannelFilters() {
val wc = createWalletConnectServiceWatcher()
channel.typedFilters = listOfNotNull(wc).ifEmpty { null }
}
override fun auth(relay: Relay, challenge: String) {
super.auth(relay, challenge)
val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey)
Client.send(
event,
relay.url
)
}
}

View File

@@ -1,73 +1,61 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.bechToBytes
import nostr.postr.toHex
import fr.acinq.secp256k1.Hex
object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
private var searchString: String? = null
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
val mySearchString = searchString
if (mySearchString == null) {
if (mySearchString.isNullOrBlank()) {
return null
}
val hexToWatch = try {
if (mySearchString.startsWith("npub") || mySearchString.startsWith("nsec")) {
decodePublicKey(mySearchString).toHex()
} else if (mySearchString.startsWith("note")) {
mySearchString.bechToBytes().toHex()
} else {
mySearchString
}
Nip19.uriToRoute(mySearchString)?.hex ?: Hex.decode(mySearchString).toHexKey()
} catch (e: Exception) {
// Usually when people add an incomplete npub or note.
null
}
if (hexToWatch == null) {
return null
}
// downloads all the reactions to a given event.
return listOf(
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
ids = listOfNotNull(hexToWatch)
return listOfNotNull(
hexToWatch?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
ids = listOfNotNull(hexToWatch)
)
)
),
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOfNotNull(hexToWatch)
},
hexToWatch?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOfNotNull(hexToWatch)
)
)
),
},
TypedFilter(
types = FeedType.values().toSet(),
types = setOf(FeedType.SEARCH),
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
search = mySearchString,
limit = 20
limit = 100
)
),
TypedFilter(
types = FeedType.values().toSet(),
types = setOf(FeedType.SEARCH),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ChannelMetadataEvent.kind, ChannelCreateEvent.kind, ChannelMessageEvent.kind),
search = mySearchString,
limit = 20
limit = 100
)
)
)

View File

@@ -1,8 +1,11 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -29,8 +32,8 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
val directEventsToLoad = channelsToWatch
.map { LocalCache.getOrCreateChannel(it) }
.filter { it.notes.isEmpty() }
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
.filter { it.notes.isEmpty() && it is PublicChatChannel }
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
@@ -40,7 +43,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
// downloads linked events to this event.
return TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = interestedEvents.toList()
@@ -48,13 +51,43 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
)
}
fun createLoadStreamingIfNotLoadedFilter(): List<TypedFilter>? {
val directEventsToLoad = channelsToWatch
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
.filterIsInstance<LiveActivitiesChannel>()
.filter { it.info == null }
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
if (interestedEvents.isEmpty()) {
return null
}
// downloads linked events to this event.
return directEventsToLoad.map {
it.address().let { aTag ->
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex)
)
)
}
}
}
val singleChannelChannel = requestNewChannel()
override fun updateChannelFilters() {
val reactions = createRepliesAndReactionsFilter()
val missing = createLoadEventsIfNotLoadedFilter()
val missingStreaming = createLoadStreamingIfNotLoadedFilter()
singleChannelChannel.typedFilters = listOfNotNull(reactions, missing).ifEmpty { null }
singleChannelChannel.typedFilters = (
(listOfNotNull(reactions, missing)) + (missingStreaming ?: emptyList())
).ifEmpty { null }
}
fun add(eventId: String) {

View File

@@ -2,23 +2,11 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import java.util.Date
object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<Note>()
@@ -31,24 +19,22 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null
}
val now = Date().time / 1000
return addressesToWatch.filter {
val lastTime = it.lastReactionsDownloadTime
lastTime == null || lastTime < (now - 10)
}.mapNotNull {
return addressesToWatch.mapNotNull {
it.address()?.let { aTag ->
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind, LongTextNoteEvent.kind,
ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind,
ReactionEvent.kind, RepostEvent.kind, GenericRepostEvent.kind, ReportEvent.kind,
LnZapEvent.kind, LnZapRequestEvent.kind,
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind,
PollNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind,
PeopleListEvent.kind, BookmarkListEvent.kind
),
tags = mapOf("a" to listOf(aTag.toTag())),
since = it.lastReactionsDownloadTime
since = it.lastReactionsDownloadTime,
limit = 1000 // Max amount of "replies" to download on a specific event.
)
)
}
@@ -62,21 +48,26 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null
}
val now = Date().time / 1000
return addressesToWatch.filter {
val lastTime = it.lastReactionsDownloadTime
lastTime == null || lastTime < (now - 10)
}.mapNotNull {
return addressesToWatch.mapNotNull {
it.address()?.let { aTag ->
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex.substring(0, 8))
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(aTag.kind),
authors = listOf(aTag.pubKeyHex)
)
)
)
} else {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex)
)
)
}
}
}
}
@@ -88,26 +79,27 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null
}
val now = Date().time / 1000
return reactionsToWatch.filter {
val lastTime = it.lastReactionsDownloadTime
lastTime == null || lastTime < (now - 10)
}.map {
return reactionsToWatch.map {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind,
LongTextNoteEvent.kind,
ReactionEvent.kind,
RepostEvent.kind,
GenericRepostEvent.kind,
ReportEvent.kind,
LnZapEvent.kind,
LnZapRequestEvent.kind
LnZapRequestEvent.kind,
PollNoteEvent.kind,
HighlightEvent.kind,
AudioTrackEvent.kind,
PinListEvent.kind
),
tags = mapOf("e" to listOf(it.idHex)),
since = it.lastReactionsDownloadTime
since = it.lastReactionsDownloadTime,
limit = 1000 // Max amount of "replies" to download on a specific event.
)
)
}
@@ -133,22 +125,26 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
// downloads linked events to this event.
return listOf(
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind,
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind
),
ids = interestedEvents.toList()
)
)
)
}
val singleEventChannel = requestNewChannel { time ->
val singleEventChannel = requestNewChannel { time, relayUrl ->
checkNotInMainThread()
eventsToWatch.forEach {
it.lastReactionsDownloadTime = time
val eose = it.lastReactionsDownloadTime[relayUrl]
if (eose == null) {
it.lastReactionsDownloadTime = it.lastReactionsDownloadTime + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()
@@ -160,26 +156,34 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val addresses = createAddressFilter()
val addressReactions = createTagToAddressFilter()
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses, addressReactions).flatten().ifEmpty { null }
singleEventChannel.typedFilters = listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null }
}
fun add(eventId: Note) {
eventsToWatch = eventsToWatch.plus(eventId)
invalidateFilters()
if (!eventsToWatch.contains(eventId)) {
eventsToWatch = eventsToWatch.plus(eventId)
invalidateFilters()
}
}
fun remove(eventId: Note) {
eventsToWatch = eventsToWatch.minus(eventId)
invalidateFilters()
if (eventsToWatch.contains(eventId)) {
eventsToWatch = eventsToWatch.minus(eventId)
invalidateFilters()
}
}
fun addAddress(aTag: Note) {
addressesToWatch = addressesToWatch.plus(aTag)
invalidateFilters()
fun addAddress(addressableNote: Note) {
if (!addressesToWatch.contains(addressableNote)) {
addressesToWatch = addressesToWatch.plus(addressableNote)
invalidateFilters()
}
}
fun removeAddress(aTag: Note) {
addressesToWatch = addressesToWatch.minus(aTag)
invalidateFilters()
fun removeAddress(addressableNote: Note) {
if (addressesToWatch.contains(addressableNote)) {
addressesToWatch = addressesToWatch.minus(addressableNote)
invalidateFilters()
}
}
}

View File

@@ -3,7 +3,8 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -15,7 +16,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
return usersToWatch.filter { it.info?.latestMetadata == null }.map {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(it.pubkeyHex),
@@ -30,17 +31,26 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
return usersToWatch.map {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
since = it.latestReportTime
since = it.latestEOSEs
)
)
}
}
val userChannel = requestNewChannel() {
val userChannel = requestNewChannel() { time, relayUrl ->
usersToWatch.forEach {
val eose = it.latestEOSEs[relayUrl]
if (eose == null) {
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()
@@ -54,12 +64,16 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
}
fun add(user: User) {
usersToWatch = usersToWatch.plus(user)
invalidateFilters()
if (!usersToWatch.contains(user)) {
usersToWatch = usersToWatch.plus(user)
invalidateFilters()
}
}
fun remove(user: User) {
usersToWatch = usersToWatch.minus(user)
invalidateFilters()
if (usersToWatch.contains(user)) {
usersToWatch = usersToWatch.minus(user)
invalidateFilters()
}
}
}

View File

@@ -1,7 +1,7 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -18,14 +18,14 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
.ifEmpty { null } ?: return null
return TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
ids = eventsToLoad.map { it.substring(0, 8) }
)
)
}
val loadEventsChannel = requestNewChannel() {
val loadEventsChannel = requestNewChannel() { _, _ ->
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()

View File

@@ -1,34 +1,21 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
var user: User? = null
fun loadUserProfile(userId: String?) {
if (userId != null) {
user = LocalCache.getOrCreateUser(userId)
} else {
user = null
}
resetFilters()
fun loadUserProfile(user: User?) {
this.user = user
}
fun createUserInfoFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(it.pubkeyHex),
@@ -39,9 +26,18 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
fun createUserPostsFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
kinds = listOf(
TextNoteEvent.kind,
GenericRepostEvent.kind,
RepostEvent.kind,
LongTextNoteEvent.kind,
AudioTrackEvent.kind,
PinListEvent.kind,
PollNoteEvent.kind,
HighlightEvent.kind
),
authors = listOf(it.pubkeyHex),
limit = 200
)
@@ -50,7 +46,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
fun createUserReceivedZapsFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(LnZapEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex))
@@ -60,7 +56,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
fun createFollowFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ContactListEvent.kind),
authors = listOf(it.pubkeyHex),
@@ -71,7 +67,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
fun createFollowersFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(ContactListEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex))
@@ -81,7 +77,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
fun createAcceptedAwardsFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind),
authors = listOf(it.pubkeyHex),
@@ -90,9 +86,20 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
)
}
fun createBookmarksFilter() = user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind, AppRecommendationEvent.kind),
authors = listOf(it.pubkeyHex),
limit = 100
)
)
}
fun createReceivedAwardsFilter() = user?.let {
TypedFilter(
types = FeedType.values().toSet(),
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(BadgeAwardEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
@@ -111,7 +118,8 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
createFollowersFilter(),
createUserReceivedZapsFilter(),
createAcceptedAwardsFilter(),
createReceivedAwardsFilter()
createReceivedAwardsFilter(),
createBookmarksFilter()
).ifEmpty { null }
}
}

View File

@@ -0,0 +1,61 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
fun createContextualFilter(): TypedFilter? {
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)
val followKeys = follows?.map {
it.substring(0, 6)
}
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = followKeys,
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
limit = 200,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
)
)
}
fun createFollowTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultStoriesFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
tags = mapOf(
"t" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
)
)
}
val videoFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
}
}

View File

@@ -0,0 +1,40 @@
package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.BuildConfig
import okhttp3.Request
@Immutable
data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
object OnlineChecker {
val checkOnlineCache = LruCache<String, OnlineCheckResult>(100)
val fiveMinutes = 1000 * 60 * 5
fun isOnline(url: String?): Boolean {
checkNotInMainThread()
if (url.isNullOrBlank()) return false
if ((checkOnlineCache.get(url)?.timeInMs ?: 0) > System.currentTimeMillis() - fiveMinutes) {
return checkOnlineCache.get(url).online
}
return try {
val request = Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
val result = HttpClient.getHttpClient().newCall(request).execute().code == 200
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
result
} catch (e: Exception) {
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false))
Log.e("LiveActivities", "Failed to check streaming url $url", e)
false
}
}
}

View File

@@ -0,0 +1,108 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
fun String.isUTF16Char(pos: Int): Boolean {
println("Test $pos ${Character.charCount(this.codePointAt(pos))}")
return Character.charCount(this.codePointAt(pos)) == 2
}
fun String.firstFullCharOld(): String {
return when (this.length) {
0, 1 -> return this
2, 3 -> return if (isUTF16Char(0)) this.take(2) else this.take(1)
else -> {
val first = isUTF16Char(0)
val second = isUTF16Char(2)
if (first && second) {
this.take(4)
} else if (first) {
this.take(2)
} else {
this.take(1)
}
}
}
}
fun String.firstFullChar(): String {
var isInJoin = false
var hasHadSecondChance = false
var start = 0
var previousCharLength = 0
var next: Int
var codePoint: Int
var i = 0
while (i < this.length) {
codePoint = codePointAt(i)
// Skips if it starts with the join char 0x200D
if (codePoint == 0x200D && previousCharLength == 0) {
next = offsetByCodePoints(i, 1)
start = next
} else {
// If join, searches for the next char
if (codePoint == 0xFE0F) {
} else if (codePoint == 0x200D) {
isInJoin = true
} else {
// stops when two chars are not joined together
if (previousCharLength > 0 && !isInJoin) {
if (Character.charCount(codePoint) == 1 || hasHadSecondChance) {
break
} else {
hasHadSecondChance = true
}
} else {
hasHadSecondChance = false
}
isInJoin = false
}
// next char to evaluate
next = offsetByCodePoints(i, 1)
previousCharLength += (next - i)
}
i = next
}
// if ends in join, then seachers backwards until a char is found.
if (isInJoin) {
i = previousCharLength - 1
while (i > 0) {
if (this[i].code == 0x200D) {
previousCharLength -= 1
} else {
break
}
i -= 1
}
}
return substring(start, start + previousCharLength)
}
fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
if (length <= 2) {
return firstFullChar()
}
if (this[0] == ':') {
// makes sure an emoji exists
val emojiParts = this.split(":", limit = 3)
if (emojiParts.size >= 2) {
val emojiName = emojiParts[1]
val emojiUrl = tags.lists.firstOrNull() { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
if (emojiUrl != null) {
return ":$emojiName:$emojiUrl"
}
}
}
return firstFullChar()
}

View File

@@ -0,0 +1,22 @@
package com.vitorpamplona.amethyst.service.connectivitystatus
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
object ConnectivityStatus {
private val onMobileData = mutableStateOf(false)
val isOnMobileData: MutableState<Boolean> = onMobileData
private val onWifi = mutableStateOf(false)
val isOnWifi: MutableState<Boolean> = onWifi
fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) {
if (onMobileData.value != isOnMobileData) {
onMobileData.value = isOnMobileData
}
if (onWifi.value != isOnWifi) {
onWifi.value = isOnWifi
}
}
}

View File

@@ -1,6 +1,9 @@
package com.vitorpamplona.amethyst.service.lnurl
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -9,13 +12,13 @@ import kotlinx.coroutines.withContext
import nostr.postr.Bech32
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.math.BigDecimal
import java.net.URLEncoder
class LightningAddressResolver {
val client = OkHttpClient.Builder().build()
class LightningAddressResolver() {
val client = HttpClient.getHttpClient()
fun assembleUrl(lnaddress: String): String? {
val parts = lnaddress.split("@")
@@ -43,6 +46,8 @@ class LightningAddressResolver {
}
private suspend fun fetchLightningAddressJsonSuspend(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
checkNotInMainThread()
val url = assembleUrl(lnaddress)
if (url == null) {
@@ -50,28 +55,32 @@ class LightningAddressResolver {
return
}
withContext(Dispatchers.IO) {
val request: Request = Request.Builder()
.header("User-Agent", "Amethyst")
.url(url)
.build()
try {
withContext(Dispatchers.IO) {
val request: Request = Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress is correct")
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress is correct")
}
}
}
}
override fun onFailure(call: Call, e: java.io.IOException) {
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
e.printStackTrace()
}
})
override fun onFailure(call: Call, e: java.io.IOException) {
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
e.printStackTrace()
}
})
}
} catch (e: Exception) {
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
}
}
@@ -95,7 +104,7 @@ class LightningAddressResolver {
}
val request: Request = Request.Builder()
.header("User-Agent", "Amethyst")
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
@@ -128,12 +137,14 @@ class LightningAddressResolver {
)
}
fun lnAddressInvoice(lnaddress: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
fun lnAddressInvoice(lnaddress: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) {
val mapper = jacksonObjectMapper()
fetchLightningAddressJson(
lnaddress,
onSuccess = { lnAddressJson ->
onProgress(0.4f)
val lnurlp = try {
mapper.readTree(lnAddressJson)
} catch (t: Throwable) {
@@ -156,6 +167,8 @@ class LightningAddressResolver {
message,
if (allowsNostr) nostrRequest else null,
onSuccess = {
onProgress(0.6f)
val lnInvoice = try {
mapper.readTree(it)
} catch (t: Throwable) {
@@ -163,8 +176,15 @@ class LightningAddressResolver {
null
}
lnInvoice?.get("pr")?.asText()?.let { pr ->
onSuccess(pr)
lnInvoice?.get("pr")?.asText()?.ifBlank { null }?.let { pr ->
// Forces LN Invoice amount to be the requested amount.
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
if (invoiceAmount.multiply(BigDecimal(1000)).toLong() == BigDecimal(milliSats).toLong()) {
onProgress(0.7f)
onSuccess(pr)
} else {
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server")
}
} ?: onError("Invoice Not Created (element pr not found in the resulting JSON)")
},
onError = onError

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service.lnurl
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import java.math.BigDecimal
import java.util.Locale
import java.util.regex.Pattern
@@ -98,11 +99,11 @@ object LnInvoiceUtil {
* @return invoice amount in bitcoins, zero if the invoice has no amount
* @throws RuntimeException if invoice format is incorrect
*/
fun getAmount(invoice: String): BigDecimal {
private fun getAmount(invoice: String): BigDecimal {
try {
decodeUnlimitedLength(invoice) // checksum must match
} catch (e: AddressFormatException) {
throw IllegalArgumentException("Cannot decode invoice", e)
throw IllegalArgumentException("Cannot decode invoice: $invoice", e)
}
val matcher = invoicePattern.matcher(invoice)
@@ -121,6 +122,8 @@ object LnInvoiceUtil {
}
fun getAmountInSats(invoice: String): BigDecimal {
checkNotInMainThread()
return getAmount(invoice).multiply(BigDecimal(100000000))
}

View File

@@ -0,0 +1,29 @@
package com.vitorpamplona.amethyst.service.lnurl
import java.util.regex.Pattern
object LnWithdrawalUtil {
private val withdrawalPattern = Pattern.compile(
"lnurl.+",
Pattern.CASE_INSENSITIVE
)
/**
* Finds LN withdrawal in the provided input string and returns it.
* For example for input = "aaa bbb lnbc1xxx ccc" it will return "lnbc1xxx"
* It will only return the first withdrawal found in the input.
*
* @return the invoice if it was found. null for null input or if no invoice is found
*/
fun findWithdrawal(input: String?): String? {
if (input == null) {
return null
}
val matcher = withdrawalPattern.matcher(input)
return if (matcher.find()) {
matcher.group()
} else {
null
}
}
}

View File

@@ -1,7 +1,8 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import com.vitorpamplona.amethyst.model.toByteArray
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.nip19.Tlv
import fr.acinq.secp256k1.Hex
@@ -9,12 +10,13 @@ import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
@Immutable
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String {
val kind = kind.toByteArray()
val author = pubKeyHex.toByteArray()
val author = pubKeyHex.hexToByteArray()
val dTag = dTag.toByteArray(Charsets.UTF_8)
val relay = relay?.toByteArray(Charsets.UTF_8)

View File

@@ -0,0 +1,41 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.UserMetadata
import java.io.ByteArrayInputStream
@Immutable
class AppDefinitionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun appMetaData() = try {
MetadataEvent.metadataParser.readValue(
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
UserMetadata::class.java
)
} catch (e: Exception) {
e.printStackTrace()
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
null
}
fun supportedKinds() = tags.filter { it.size > 1 && it[0] == "k" }.mapNotNull {
runCatching { it[1].toInt() }.getOrNull()
}
fun publishedAt() = tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)
companion object {
const val kind = 31990
}
}

View File

@@ -0,0 +1,27 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
@Immutable
class AppRecommendationEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
fun recommendations() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
ATag.parse(it[1], it.getOrNull(2))
}
fun forKind() = runCatching { dTag().toInt() }.getOrNull()
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 31989
}
}

View File

@@ -0,0 +1,63 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
@Immutable
class AudioTrackEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) }
fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
fun price() = tags.firstOrNull { it.size > 1 && it[0] == PRICE }?.get(1)
fun cover() = tags.firstOrNull { it.size > 1 && it[0] == COVER }?.get(1)
fun subject() = tags.firstOrNull { it.size > 1 && it[0] == SUBJECT }?.get(1)
fun media() = tags.firstOrNull { it.size > 1 && it[0] == MEDIA }?.get(1)
companion object {
const val kind = 31337
private const val TYPE = "c"
private const val PRICE = "price"
private const val COVER = "cover"
private const val SUBJECT = "subject"
private const val MEDIA = "media"
fun create(
type: String,
media: String,
price: String? = null,
cover: String? = null,
subject: String? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): AudioTrackEvent {
val tags = listOfNotNull(
listOf(MEDIA, media),
listOf(TYPE, type),
price?.let { listOf(PRICE, it) },
cover?.let { listOf(COVER, it) },
subject?.let { listOf(SUBJECT, it) }
)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
return AudioTrackEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
data class Participant(val key: String, val role: String?)

View File

@@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
@Immutable
class BadgeAwardEvent(
id: HexKey,
pubKey: HexKey,
@@ -10,13 +12,9 @@ class BadgeAwardEvent(
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
fun awardees() = taggedUsers()
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun awardDefinition() = taggedAddresses()
companion object {
const val kind = 8

View File

@@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
@Immutable
class BadgeDefinitionEvent(
id: HexKey,
pubKey: HexKey,
@@ -9,14 +11,14 @@ class BadgeDefinitionEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag(), null)
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1)
fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == "thumb" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
companion object {
const val kind = 30009

View File

@@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
@Immutable
class BadgeProfilesEvent(
id: HexKey,
pubKey: HexKey,
@@ -9,7 +11,7 @@ class BadgeProfilesEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
@@ -18,10 +20,11 @@ class BadgeProfilesEvent(
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag(), null)
override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 30008
const val standardDTAg = "profile_badges"
}
}

View File

@@ -1,8 +1,13 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.tagSearch
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.nip19.Nip19.nip19regex
@Immutable
open class BaseTextNoteEvent(
id: HexKey,
pubKey: HexKey,
@@ -13,37 +18,102 @@ open class BaseTextNoteEvent(
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = taggedUsers()
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
open fun replyTos() = taggedEvents()
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
private var citedUsersCache: Set<HexKey>? = null
private var citedNotesCache: Set<HexKey>? = null
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
fun citedUsers(): Set<HexKey> {
citedUsersCache?.let { return it }
val matcher = tagSearch.matcher(content)
val returningList = mutableSetOf<String>()
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { tags[it.toInt()] }
if (tag != null && tag.size > 1 && tag[0] == "p") {
returningList.add(tag[1])
}
} catch (e: Exception) {
}
}
val matcher2 = nip19regex.matcher(content)
while (matcher2.find()) {
val uriScheme = matcher2.group(1) // nostr:
val type = matcher2.group(2) // npub1
val key = matcher2.group(3) // bech32
val additionalChars = matcher2.group(4) // additional chars
try {
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
if (parsed != null) {
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
if (tag != null && tag[0] == "p") {
returningList.add(tag[1])
}
}
} catch (e: Exception) {
Log.w("Unable to parse cited users that matched a NIP19 regex", e)
}
}
citedUsersCache = returningList
return returningList
}
fun findCitations(): Set<String> {
var citations = mutableSetOf<String>()
fun findCitations(): Set<HexKey> {
citedNotesCache?.let { return it }
val citations = mutableSetOf<HexKey>()
// Removes citations from replies:
val matcher = tagSearch.matcher(content)
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { tags[it.toInt()] }
if (tag != null && tag[0] == "e") {
if (tag != null && tag.size > 1 && tag[0] == "e") {
citations.add(tag[1])
}
if (tag != null && tag[0] == "a") {
if (tag != null && tag.size > 1 && tag[0] == "a") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
val matcher2 = nip19regex.matcher(content)
while (matcher2.find()) {
val uriScheme = matcher2.group(1) // nostr:
val type = matcher2.group(2) // npub1
val key = matcher2.group(3) // bech32
val additionalChars = matcher2.group(4) // additional chars
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
if (parsed != null) {
try {
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
if (tag != null && tag[0] == "e") {
citations.add(tag[1])
}
if (tag != null && tag[0] == "a") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
}
citedNotesCache = citations
return citations
}
fun tagsWithoutCitations(): List<String> {
val repliesTo = replyTos()
val tagAddresses = taggedAddresses().map { it.toTag() }
val tagAddresses = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.kind }.map { it.toTag() }
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
val citations = findCitations()

View File

@@ -0,0 +1,56 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
@Immutable
class BookmarkListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 30001
fun create(
name: String = "",
events: List<String>? = null,
users: List<String>? = null,
addresses: List<ATag>? = null,
privEvents: List<String>? = null,
privUsers: List<String>? = null,
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): BookmarkListEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
events?.forEach {
tags.add(listOf("e", it))
}
users?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return BookmarkListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
}

View File

@@ -1,11 +1,13 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelCreateEvent(
id: HexKey,
pubKey: HexKey,
@@ -24,7 +26,7 @@ class ChannelCreateEvent(
companion object {
const val kind = 40
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelCreateEvent {
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelCreateEvent {
val content = try {
if (channelInfo != null) {
gson.toJson(channelInfo)
@@ -44,5 +46,6 @@ class ChannelCreateEvent(
}
}
data class ChannelData(var name: String?, var about: String?, var picture: String?)
@Immutable
data class ChannelData(val name: String?, val about: String?, val picture: String?)
}

View File

@@ -1,10 +1,12 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelHideMessageEvent(
id: HexKey,
pubKey: HexKey,
@@ -12,13 +14,19 @@ class ChannelHideMessageEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 43
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent {
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags =
messagesToHide?.map {

View File

@@ -1,10 +1,12 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelMessageEvent(
id: HexKey,
pubKey: HexKey,
@@ -12,16 +14,30 @@ class ChannelMessageEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun replyTos() = tags.filter { it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
override fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
override fun replyTos() = tags.filter { it.firstOrNull() == "e" && it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 42
fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent {
fun create(
message: String,
channel: String,
replyTos: List<String>? = null,
mentions: List<String>? = null,
zapReceiver: String?,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
): ChannelMessageEvent {
val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf(
@@ -33,6 +49,15 @@ class ChannelMessageEvent(
mentions?.forEach {
tags.add(listOf("p", it))
}
zapReceiver?.let {
tags.add(listOf("zap", it))
}
if (markAsSensitive) {
tags.add(listOf("content-warning", ""))
}
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
@@ -40,3 +65,7 @@ class ChannelMessageEvent(
}
}
}
interface IsInPublicChatChannel {
open fun channel(): String?
}

View File

@@ -1,11 +1,13 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelMetadataEvent(
id: HexKey,
pubKey: HexKey,
@@ -13,8 +15,9 @@ class ChannelMetadataEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun channelInfo() =
try {
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
@@ -26,7 +29,7 @@ class ChannelMetadataEvent(
companion object {
const val kind = 41
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMetadataEvent {
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMetadataEvent {
val content =
if (newChannelInfo != null) {
gson.toJson(newChannelInfo)

View File

@@ -1,10 +1,12 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelMuteUserEvent(
id: HexKey,
pubKey: HexKey,
@@ -12,14 +14,19 @@ class ChannelMuteUserEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
override fun channel() = tags.firstOrNull {
it.size > 3 && it[0] == "e" && it[3] == "root"
}?.get(1) ?: tags.firstOrNull {
it.size > 1 && it[0] == "e"
}?.get(1)
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 44
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent {
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent {
val content = reason
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags =

View File

@@ -0,0 +1,77 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
@Immutable
class ClassifiedsEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let {
Price(it[1], it.getOrNull(2), it.getOrNull(3))
}
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun publishedAt() = try {
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
} catch (_: Exception) {
null
}
companion object {
const val kind = 30402
fun create(
dTag: String,
title: String?,
image: String?,
summary: String?,
price: Price?,
location: String?,
publishedAt: Long?,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): ClassifiedsEvent {
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", dTag))
title?.let { tags.add(listOf("title", it)) }
image?.let { tags.add(listOf("image", it)) }
summary?.let { tags.add(listOf("summary", it)) }
price?.let {
if (it.frequency != null && it.currency != null) {
tags.add(listOf("price", it.amount, it.currency, it.frequency))
} else if (it.currency != null) {
tags.add(listOf("price", it.amount, it.currency))
} else {
tags.add(listOf("price", it.amount))
}
}
location?.let { tags.add(listOf("location", it)) }
publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) }
title?.let { tags.add(listOf("title", it)) }
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
data class Price(val amount: String, val currency: String?, val frequency: String?)

View File

@@ -0,0 +1,42 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
@Immutable
class CommunityDefinitionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun rules() = tags.firstOrNull { it.size > 1 && it[0] == "rules" }?.get(1)
fun moderators() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
companion object {
const val kind = 34550
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CommunityDefinitionEvent {
val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
return CommunityDefinitionEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}

View File

@@ -0,0 +1,63 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
@Immutable
class CommunityPostApprovalEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun containedPost(): Event? = try {
content.ifBlank { null }?.let {
fromJson(it, Client.lenient)
}
} catch (e: Exception) {
Log.w("CommunityPostEvent", "Failed to Parse Community Approval Contained Post of $id with $content")
null
}
fun communities() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
val aTag = ATag.parse(it[1], it.getOrNull(2))
if (aTag?.kind == CommunityDefinitionEvent.kind) {
aTag
} else {
null
}
}
fun approvedEvents() = tags.filter {
it.size > 1 && (it[0] == "e" || (it[0] == "a" && ATag.parse(it[1], null)?.kind != CommunityDefinitionEvent.kind))
}.map {
it[1]
}
companion object {
const val kind = 4550
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
val content = approvedPost.toJson()
val communities = listOf("a", community.address().toTag())
val replyToPost = listOf("e", approvedPost.id())
val replyToAuthor = listOf("p", approvedPost.pubKey())
val kind = listOf("k", "${approvedPost.kind()}")
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(communities, replyToPost, replyToAuthor, kind)
val id = generateId(pubKey, createdAt, GenericRepostEvent.kind, tags, content)
val sig = Utils.sign(id, privateKey)
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}

View File

@@ -1,15 +1,19 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
data class Contact(val pubKeyHex: String, val relayUri: String?)
@Stable
class ContactListEvent(
id: HexKey,
pubKey: HexKey,
@@ -21,23 +25,32 @@ class ContactListEvent(
// This function is only used by the user logged in
// But it is used all the time.
val verifiedFollowKeySet: Set<HexKey> by lazy {
tags.filter { it[0] == "p" }.mapNotNull {
it.getOrNull(1)?.let { unverifiedHex: String ->
try {
decodePublicKey(unverifiedHex).toHexKey()
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull {
try {
decodePublicKey(it[1]).toHexKey()
} catch (e: Exception) {
Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e)
null
}
}.toSet()
}
val verifiedFollowTagSet: Set<String> by lazy {
unverifiedFollowTagSet().map { it.lowercase() }.toSet()
}
val verifiedFollowCommunitySet: Set<String> by lazy {
unverifiedFollowAddressSet().toSet()
}
val verifiedFollowKeySetAndMe: Set<HexKey> by lazy {
verifiedFollowKeySet + pubKey
}
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) }
fun follows() = tags.filter { it[0] == "p" }.mapNotNull {
try {
@@ -48,6 +61,10 @@ class ContactListEvent(
}
}
fun followsTags() = tags.filter { it[0] == "t" }.mapNotNull {
it.getOrNull(2)
}
fun relays(): Map<String, ReadWrite>? = try {
if (content.isNotEmpty()) {
gson.fromJson(content, object : TypeToken<Map<String, ReadWrite>>() {}.type) as Map<String, ReadWrite>
@@ -62,20 +79,152 @@ class ContactListEvent(
companion object {
const val kind = 3
fun create(follows: List<Contact>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent {
fun createFromScratch(
followUsers: List<Contact>,
followTags: List<String>,
followCommunities: List<ATag>,
followEvents: List<String>,
relayUse: Map<String, ReadWrite>?,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): ContactListEvent {
val content = if (relayUse != null) {
gson.toJson(relayUse)
} else {
""
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = follows.map {
val tags = followUsers.map {
if (it.relayUri != null) {
listOf("p", it.pubKeyHex, it.relayUri)
} else {
listOf("p", it.pubKeyHex)
}
} + followTags.map {
listOf("t", it)
} + followEvents.map {
listOf("e", it)
} + followCommunities.map {
if (it.relay != null) {
listOf("a", it.toTag(), it.relay)
} else {
listOf("a", it.toTag())
}
}
return create(
content = content,
tags = tags,
privateKey = privateKey,
createdAt = createdAt
)
}
fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
privateKey = privateKey,
createdAt = createdAt
)
}
fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
privateKey = privateKey,
createdAt = createdAt
)
}
fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("t", hashtag)),
privateKey = privateKey,
createdAt = createdAt
)
}
fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
privateKey = privateKey,
createdAt = createdAt
)
}
fun followEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("e", idHex)),
privateKey = privateKey,
createdAt = createdAt
)
}
fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedEvent(idHex)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex },
privateKey = privateKey,
createdAt = createdAt
)
}
fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOfNotNull("a", aTag.toTag(), aTag.relay)),
privateKey = privateKey,
createdAt = createdAt
)
}
fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() },
privateKey = privateKey,
createdAt = createdAt
)
}
fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
val content = if (relayUse != null) {
gson.toJson(relayUse)
} else {
""
}
return create(
content = content,
tags = earlierVersion.tags,
privateKey = privateKey,
createdAt = createdAt
)
}
fun create(content: String, tags: List<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())

View File

@@ -1,10 +1,12 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class DeletionEvent(
id: HexKey,
pubKey: HexKey,
@@ -18,7 +20,7 @@ class DeletionEvent(
companion object {
const val kind = 5
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent {
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): DeletionEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = deleteEvents.map { listOf("e", it) }

View File

@@ -0,0 +1,56 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
@Immutable
class EmojiPackEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 30030
fun create(
name: String = "",
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): EmojiPackEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
}
@Immutable
data class EmojiUrl(val code: String, val url: String) {
fun encode(): String {
return ":$code:$url"
}
companion object {
fun decode(encodedEmojiSetup: String): EmojiUrl? {
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
return if (emojiParts.size > 2) {
EmojiUrl(emojiParts[1], emojiParts[2])
} else {
null
}
}
}
}

View File

@@ -0,0 +1,43 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
@Immutable
class EmojiPackSelectionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = ""
override fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 10030
fun create(
listOfEmojiPacks: List<ATag>?,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): EmojiPackSelectionEvent {
val msg = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
listOfEmojiPacks?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
return EmojiPackSelectionEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
}

View File

@@ -1,17 +1,23 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.google.gson.*
import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.nip19.Nip19
import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1
import nostr.postr.Utils
import nostr.postr.toHex
import java.lang.reflect.Type
import java.math.BigDecimal
import java.security.MessageDigest
import java.util.*
@Immutable
open class Event(
val id: HexKey,
@SerializedName("pubkey") val pubKey: HexKey,
@@ -37,9 +43,107 @@ open class Event(
override fun toJson(): String = gson.toJson(this)
fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" }
override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex }
override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
override fun isSensitive() = tags.any {
(it.size > 0 && it[0].equals("content-warning", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
(it.size > 1 && it[0] == "t" && it[1].equals("nude", true))
}
override fun zapraiserAmount() = tags.firstOrNull() {
(it.size > 1 && it[0] == "zapraiser")
}?.get(1)?.toLongOrNull()
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex }
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes }
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
override fun firstIsTaggedAddressableNote(addressableNotes: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1)
override fun isTaggedAddressableKind(kind: Int): Boolean {
val kindStr = kind.toString()
return tags.any { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
}
override fun getTagOfAddressableKind(kind: Int): ATag? {
val kindStr = kind.toString()
val aTag = tags
.firstOrNull { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
?.getOrNull(1)
?: return null
return ATag.parse(aTag, null)
}
override fun getPoWRank(): Int {
var rank = 0
for (i in 0..id.length) {
if (id[i] == '0') {
rank += 4
} else if (id[i] in '4'..'7') {
rank += 1
break
} else if (id[i] in '2'..'3') {
rank += 2
break
} else if (id[i] == '1') {
rank += 3
break
} else {
break
}
}
return rank
}
override fun getReward(): BigDecimal? {
return try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
} catch (e: Exception) {
null
}
}
open fun toNIP19(): String {
return if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr()
} else {
Nip19.createNEvent(id, pubKey, kind, null)
}
}
fun toNostrUri(): String {
return "nostr:${toNIP19()}"
}
/**
* Checks if the ID is correct and then if the pubKey's secret key signed the event.
@@ -60,11 +164,12 @@ open class Event(
}
override fun hasValidSignature(): Boolean {
if (!id.contentEquals(generateId())) {
return false
return try {
id.contentEquals(generateId()) && secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
} catch (e: Exception) {
Log.e("Event", "Fail checking if event $id has a valid signature", e)
false
}
return secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
}
private fun generateId(): String {
@@ -78,7 +183,7 @@ open class Event(
.replace("\\u2028", "\u2028")
.replace("\\u2029", "\u2029")
return sha256.digest(rawEventJson.toByteArray()).toHexKey()
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray()).toHexKey()
}
private class EventDeserializer : JsonDeserializer<Event> {
@@ -152,13 +257,14 @@ open class Event(
companion object {
private val secp256k1 = Secp256k1.get()
val sha256: MessageDigest = MessageDigest.getInstance("SHA-256")
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(Event::class.java, EventSerializer())
.registerTypeAdapter(Event::class.java, EventDeserializer())
.registerTypeAdapter(ByteArray::class.java, ByteArraySerializer())
.registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer())
.registerTypeAdapter(Response::class.java, ResponseDeserializer())
.registerTypeAdapter(Request::class.java, RequestDeserializer())
.create()
fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
@@ -166,25 +272,47 @@ open class Event(
fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) {
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.kind -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
ClassifiedsEvent.kind -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
NNSEvent.kind -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.kind -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient)
RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -200,11 +328,19 @@ open class Event(
tags,
content
)
// GSON decided to hardcode these replacements.
// They break Nostr's hash check.
// These lines revert their code.
// https://github.com/google/gson/issues/2295
val rawEventJson = gson.toJson(rawEvent)
return sha256.digest(rawEventJson.toByteArray())
.replace("\\u2028", "\u2028")
.replace("\\u2029", "\u2029")
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray())
}
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event {
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey).toHexKey()
@@ -212,3 +348,9 @@ open class Event(
}
}
}
@Immutable
interface AddressableEvent {
fun dTag(): String
fun address(): ATag
}

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