mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 20:33:32 +00:00
Merge branch 'prep-v0.2.0' into 'main'
Prep v0.2.0 See merge request mysocialportal/relatica!22
This commit is contained in:
commit
e7c99b5021
21 changed files with 359 additions and 313 deletions
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,6 +1,33 @@
|
||||||
# Relatica Change Log
|
# Relatica Change Log
|
||||||
|
|
||||||
## Version 0.1.0b3 (beta)
|
## Version 0.2.0 (beta), 15 March 2023
|
||||||
|
|
||||||
|
* Changes
|
||||||
|
* Uses Google's Material 3 UI instead of Material 2
|
||||||
|
* Drawer idiom replaces the menu screen idiom. Drawer contains list of logged in accounts at the top to quickly
|
||||||
|
switch between, followed by the menu items previously on the menu screen.
|
||||||
|
* "Sign In Screen" now also the "Manage Accounts" screen
|
||||||
|
* Username/password settings split the username/servername field so can use the email address as the login like
|
||||||
|
with
|
||||||
|
the website
|
||||||
|
* Have sections listing logged in and logged out accounts.
|
||||||
|
* Can permanently delete previous credentials if no longer want them.
|
||||||
|
* Added back swiping down for refresh on timelines, notifications, and contacts. Also changed the progress indicator
|
||||||
|
to be a linear bar at the top
|
||||||
|
* Unread notifications counts no longer appear.
|
||||||
|
* "New Post" button is now a floating action button at the bottom right of the timeline
|
||||||
|
* Fixes
|
||||||
|
* Toggle like status on nested comments no longer generates an error.
|
||||||
|
* New Features
|
||||||
|
* Support for OAuth Login as well as username/password
|
||||||
|
* Support for multiple concurrent logins and switching between them
|
||||||
|
* Added paging to notifications
|
||||||
|
* Ability to list who liked or reshared a post by clicking on the respective summary.
|
||||||
|
* Unread direct messages indicators show up in the notifications panel
|
||||||
|
* Data Pages manager allows for re-requesting data from server based on previous calls. Currently used only for
|
||||||
|
notifications.
|
||||||
|
|
||||||
|
## Version 0.1.0b3 (beta), 30 January 2023
|
||||||
|
|
||||||
* Changes
|
* Changes
|
||||||
* The timeline names have been changed to:
|
* The timeline names have been changed to:
|
||||||
|
@ -17,6 +44,8 @@
|
||||||
* Keyboard type on login screen for username field is now "email" type
|
* Keyboard type on login screen for username field is now "email" type
|
||||||
* New Features
|
* New Features
|
||||||
* A combined network status/refresh icon on timeline, notifications, and gallery panel
|
* A combined network status/refresh icon on timeline, notifications, and gallery panel
|
||||||
|
* More detailed user profile screen for users which now includes things like their description, handle and common
|
||||||
|
name, etc.
|
||||||
* Paging in gallery list to incremental load of larger galleries (also sorted newest first)
|
* Paging in gallery list to incremental load of larger galleries (also sorted newest first)
|
||||||
* Image View Screen has a Carousel feature for swiping through images of a post, gallery, etc.
|
* Image View Screen has a Carousel feature for swiping through images of a post, gallery, etc.
|
||||||
* Can copy post/comment Raw HTML text, image descriptions, and Direct Message text content
|
* Can copy post/comment Raw HTML text, image descriptions, and Direct Message text content
|
||||||
|
@ -25,7 +54,7 @@
|
||||||
* Dialog Box "Menu" on posts/comments to more easily decide to navigate to it internally (Posts only), open in the
|
* Dialog Box "Menu" on posts/comments to more easily decide to navigate to it internally (Posts only), open in the
|
||||||
system's default browser, or copy the URL
|
system's default browser, or copy the URL
|
||||||
|
|
||||||
## Version 0.1.0b2 (beta)
|
## Version 0.1.0b2 (beta), 27 January 2023
|
||||||
|
|
||||||
* Fixes
|
* Fixes
|
||||||
* Scrollwheel zooming now works correctly on desktop
|
* Scrollwheel zooming now works correctly on desktop
|
||||||
|
@ -47,7 +76,7 @@
|
||||||
* Start a new conversation by searching for a known contact "type @ and start typing like in
|
* Start a new conversation by searching for a known contact "type @ and start typing like in
|
||||||
posts/comments"
|
posts/comments"
|
||||||
|
|
||||||
## Version 0.1.0 (beta)
|
## Version 0.1.0 (beta), 20 January 2023
|
||||||
|
|
||||||
* Initial public release, as an early beta.
|
* Initial public release, as an early beta.
|
||||||
* Major working features (at least in initial implementation versions):
|
* Major working features (at least in initial implementation versions):
|
||||||
|
|
11
README.md
11
README.md
|
@ -2,17 +2,19 @@
|
||||||
|
|
||||||
A Flutter application for interfacing with the Friendica social network.
|
A Flutter application for interfacing with the Friendica social network.
|
||||||
|
|
||||||
<img src="screenshots/v.0.1.0/windows/RelaticaFirstScreenshot.png" alt="Relatica v0.1.0 on Windows Screenshot" width="300px"/>
|
<img src="screenshots/v0.2.0/linux/relatica_v0.2.0_home.png" alt="Relatica v0.2.0 on Linux Home Screen Screenshot" width="300px"/>
|
||||||
|
|
||||||
|
<img src="screenshots/v0.2.0/linux/relatica_v0.2.0_drawer.png" alt="Relatica v0.2.0 on Linux Home Expanded Drawer Screenshot" width="300px"/>
|
||||||
|
|
||||||
## Project Objectives
|
## Project Objectives
|
||||||
|
|
||||||
* Have a native app client on mobile (Apple and Android) and desktop (Linux, Mac, and Windows)
|
* Have a native app client on mobile (Apple and Android) and desktop (Linux, Mac, and Windows)
|
||||||
* Providing a simpler UX for people to interact with Friendica
|
* Providing a simpler UX for people to interact with Friendica
|
||||||
* Providing a better low-bandwidth environment experience than the web-app version running in a browser
|
* Providing a better low-bandwidth environment experience than the web-app version running in a browser
|
||||||
* Reduce server side loading in the new fediverse era by doing techniques like leveraging paging of comments, local caching, and lazy loading.
|
* Reduce server side loading in the new fediverse era by doing techniques like leveraging paging of comments, local
|
||||||
|
caching, and lazy loading.
|
||||||
* Provide more intuitive mechanisms for adding things like Content Warning/Spoiler text and image ALT-text
|
* Provide more intuitive mechanisms for adding things like Content Warning/Spoiler text and image ALT-text
|
||||||
|
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
The project is currently in an early-beta state. If you'd like to use it at this time please
|
The project is currently in an early-beta state. If you'd like to use it at this time please
|
||||||
|
@ -22,7 +24,6 @@ It is possible to install it now by following the [install instructions](install
|
||||||
|
|
||||||
If you would like to contribute please see [this Developers Notes](developers.md) section.
|
If you would like to contribute please see [this Developers Notes](developers.md) section.
|
||||||
|
|
||||||
|
|
||||||
## Community and Support
|
## Community and Support
|
||||||
|
|
||||||
[Relatica Community Matrix Discussion Room](https://matrix.to/#/#relatica-community:myportal.social)
|
[Relatica Community Matrix Discussion Room](https://matrix.to/#/#relatica-community:myportal.social)
|
||||||
|
@ -30,6 +31,7 @@ If you would like to contribute please see [this Developers Notes](developers.md
|
||||||
[Issue Tracker](https://gitlab.com/mysocialportal/relatica/-/issues)
|
[Issue Tracker](https://gitlab.com/mysocialportal/relatica/-/issues)
|
||||||
|
|
||||||
### Things I could use help from community on:
|
### Things I could use help from community on:
|
||||||
|
|
||||||
* More coders and testers are always welcome
|
* More coders and testers are always welcome
|
||||||
* CI/CD
|
* CI/CD
|
||||||
* Packaging for Linux operating systems using Flatpak, Snap, etc. (or individual packages per operating system)
|
* Packaging for Linux operating systems using Flatpak, Snap, etc. (or individual packages per operating system)
|
||||||
|
@ -39,4 +41,5 @@ If you would like to contribute please see [this Developers Notes](developers.md
|
||||||
* Spreading the word
|
* Spreading the word
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Relatica is licensed with the [Mozilla Public License 2.0 (MPL)](LICENSE) copyleft license.
|
Relatica is licensed with the [Mozilla Public License 2.0 (MPL)](LICENSE) copyleft license.
|
|
@ -55,7 +55,8 @@ wanted to lay out some expectations before getting into the small details
|
||||||
|
|
||||||
### Things that work
|
### Things that work
|
||||||
|
|
||||||
* Logging in with username/password. These are stored using the OS specific key vaults
|
* Logging in with username/password and OAuth. These are stored using the OS specific key vaults
|
||||||
|
* Having multiple accounts logged in at the same time and switching between them
|
||||||
* Writing **public** posts
|
* Writing **public** posts
|
||||||
* Typing @ brings up a list of all known fediverse accounts that the app has ever seen as you
|
* Typing @ brings up a list of all known fediverse accounts that the app has ever seen as you
|
||||||
type (but not all that your server has seen)
|
type (but not all that your server has seen)
|
||||||
|
@ -84,13 +85,14 @@ wanted to lay out some expectations before getting into the small details
|
||||||
specific buttons (this will probably change in the near future)
|
specific buttons (this will probably change in the near future)
|
||||||
* Refresh notifications and contacts gets updates to that respective data (this may
|
* Refresh notifications and contacts gets updates to that respective data (this may
|
||||||
change in the near future)
|
change in the near future)
|
||||||
|
* Show list of who liked and reshared posts/comments
|
||||||
|
* Detailed information about users, such as description fields
|
||||||
|
|
||||||
### Big things I want to have working in the near future:
|
### Big things I want to have working in the near future:
|
||||||
|
|
||||||
* Show list of who liked and reshared posts/comments
|
|
||||||
* More timeline types like Comments, "By Activity", etc.
|
* More timeline types like Comments, "By Activity", etc.
|
||||||
* Better timeline UX to allow for "in-fill"
|
* Better timeline UX to allow for "in-fill"
|
||||||
* More detailed profile information for users and logged in user
|
* Fine grain details about users (last post, last update)
|
||||||
* Better data display on larger format displays by doing things like:
|
* Better data display on larger format displays by doing things like:
|
||||||
* Allowing images/thumbnails to be larger
|
* Allowing images/thumbnails to be larger
|
||||||
* Limiting maximum width of timeline columns
|
* Limiting maximum width of timeline columns
|
||||||
|
@ -124,7 +126,6 @@ wanted to lay out some expectations before getting into the small details
|
||||||
* Nitter replacement of Twitter links
|
* Nitter replacement of Twitter links
|
||||||
* User configurable Server blocking
|
* User configurable Server blocking
|
||||||
* Server-side searching tied into profiles, posts, hashtags
|
* Server-side searching tied into profiles, posts, hashtags
|
||||||
* OAuth logins
|
|
||||||
* Being able to ignore/unignore users
|
* Being able to ignore/unignore users
|
||||||
* Deleting images and entire galleries
|
* Deleting images and entire galleries
|
||||||
* Events
|
* Events
|
||||||
|
@ -134,7 +135,6 @@ wanted to lay out some expectations before getting into the small details
|
||||||
* Account creation through the application
|
* Account creation through the application
|
||||||
* Creating new forums through the application
|
* Creating new forums through the application
|
||||||
* Site administration
|
* Site administration
|
||||||
* Two-factor authentication
|
|
||||||
* Adding videos or files to posts
|
* Adding videos or files to posts
|
||||||
* Multi-account logins
|
* Multi-account logins
|
||||||
|
|
||||||
|
@ -143,13 +143,13 @@ wanted to lay out some expectations before getting into the small details
|
||||||
### Broken and hopefully fixed in the very near future:
|
### Broken and hopefully fixed in the very near future:
|
||||||
|
|
||||||
* Resharing of Diaspora federated posts is currently broken server side. All other posts should be
|
* Resharing of Diaspora federated posts is currently broken server side. All other posts should be
|
||||||
reshareable. (Already fixed in Friendica but not in the stable release yet)
|
reshareable. (Fixed in Friendica 2023.03)
|
||||||
* Content Warnings/Spoiler Text on *posts* aren't federating over to Mastodon well so only use it on
|
* Content Warnings/Spoiler Text on *posts* aren't federating over to Mastodon well so only use it on
|
||||||
Comments for now
|
Comments for now (fixed on servers running Friendica 2023.03)
|
||||||
* ALT text on images should not have any quotation marks as it breaks when federating over to
|
* ALT text on images should not have any quotation marks as it breaks when federating over to
|
||||||
Diaspora for the time being
|
Diaspora for the time being
|
||||||
* Portrait videos overflow their boxes in the timeline
|
* Portrait videos overflow their boxes in the timeline
|
||||||
* Blocked/ignored user's content is still returned by the API
|
* Blocked/ignored user's content is still returned by the API (Fixed in Friendica 2023.03)
|
||||||
* Paging for some of the endpoints either isn't wired in yet or is not working as needed server
|
* Paging for some of the endpoints either isn't wired in yet or is not working as needed server
|
||||||
side. That includes things like:
|
side. That includes things like:
|
||||||
* Friend requests
|
* Friend requests
|
||||||
|
@ -164,19 +164,12 @@ wanted to lay out some expectations before getting into the small details
|
||||||
* The only post type that is supported right now are public posts therefore all posts you write
|
* The only post type that is supported right now are public posts therefore all posts you write
|
||||||
through the app will have this privacy level.
|
through the app will have this privacy level.
|
||||||
* Notifications need to be manually refreshed.
|
* Notifications need to be manually refreshed.
|
||||||
* Responsiveness can be laggy. Sometimes hitting buttons doesn't seem to do something but it is
|
|
||||||
doing a network request. I know I need to improve that
|
|
||||||
* In galleries you need to double click on the picture to open the preview. It is remnants of an
|
|
||||||
experiment I was doing on more consistent UX which feels broken so I'm changing.
|
|
||||||
* Sometimes timelines get confused so swapping between the different groups/timelines creates a
|
* Sometimes timelines get confused so swapping between the different groups/timelines creates a
|
||||||
muddled display. Restarting the app fixes this.
|
muddled display. Restarting the app fixes this.
|
||||||
* Some images within posts, usually graphical emojis, are rendered drastically larger than they
|
* Some images within posts, usually graphical emojis, are rendered drastically larger than they
|
||||||
should be.
|
should be.
|
||||||
* On Linux you will need to re-enter your credentials each time you use the app for the first time
|
* On Linux you will need to login to the key manager and unlock it before opening the app. Some Linux versions do this
|
||||||
after logging in.
|
automatically while others do not.
|
||||||
* Groups are listed by the order they were created not alphabetically in the drop down menu
|
|
||||||
* Liking a nested comment can appear to lock up (it stays grayed out). Navigating back and forth
|
|
||||||
fixes that.
|
|
||||||
* The "in fill" problem: Timelines fill only at the ends with at most 20 posts per call. So let's
|
* The "in fill" problem: Timelines fill only at the ends with at most 20 posts per call. So let's
|
||||||
say you logged in at 09:00 and the initial pulls went from 07:00 to 09:00:
|
say you logged in at 09:00 and the initial pulls went from 07:00 to 09:00:
|
||||||
```
|
```
|
||||||
|
|
11
install.md
11
install.md
|
@ -6,12 +6,11 @@ For more information about the current beta testing program
|
||||||
|
|
||||||
# Latest Binaries:
|
# Latest Binaries:
|
||||||
|
|
||||||
* [Android v0.1.0b3](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.1.0/relatica_v0.1.0b3.apk.zip)
|
* [Android v0.2.0](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.2.0%2Frelatica_v0.2.0.apk.zip)
|
||||||
* iPhone/iPad v0.1.0b3: This is only available through TestFlight. Please contact me for access.
|
* iPhone/iPad v0.2.0: This is only available through TestFlight. Please contact me for access.
|
||||||
* [Windows (Intel) v0.1.0b3](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.1.0/Relatica_v0.1.0b3_x64_win.zip)
|
* [Windows (Intel) v0.2.0](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.2.0%2FRelatica_v0.2.0_x64_win.zip)
|
||||||
* [macOS v0.1.0b3](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.1.0/Relatica_v0.1.0b3_mac.zip)
|
* macOS v0.2.0 This is only available through TestFlight. Please contact me for access.
|
||||||
also through TestFlight
|
* [Linux (Intel Ubuntu 22) v0.2.0](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.2.0%2Frelatica_v0.2.0_linux_x64_ubuntu22.zip)
|
||||||
* [Linux (Intel Ubuntu 22) v0.1.0b3](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.1.0/relatica_v0.1.0b3_linux_x64_ubuntu22.zip)
|
|
||||||
|
|
||||||
## Mobile
|
## Mobile
|
||||||
|
|
||||||
|
|
|
@ -42,12 +42,7 @@ class AppBottomNavBar extends StatelessWidget {
|
||||||
|
|
||||||
switch (newButton) {
|
switch (newButton) {
|
||||||
case NavBarButtons.timelines:
|
case NavBarButtons.timelines:
|
||||||
try {
|
context.go(ScreenPaths.timelines);
|
||||||
Navigator.of(context)
|
|
||||||
.popUntil(ModalRoute.withName(ScreenPaths.timelines));
|
|
||||||
} catch (e) {
|
|
||||||
context.go(ScreenPaths.timelines);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case NavBarButtons.notifications:
|
case NavBarButtons.notifications:
|
||||||
context.pushNamed(ScreenPaths.notifications);
|
context.pushNamed(ScreenPaths.notifications);
|
||||||
|
|
|
@ -9,64 +9,66 @@ import '../services/auth_service.dart';
|
||||||
class StandardAppDrawer extends StatelessWidget {
|
class StandardAppDrawer extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Drawer(
|
return SafeArea(
|
||||||
child: ListView(
|
child: Drawer(
|
||||||
padding: EdgeInsets.zero,
|
child: ListView(
|
||||||
children: [
|
padding: EdgeInsets.zero,
|
||||||
...getIt<AccountsService>().loggedInProfiles.map(
|
children: [
|
||||||
(p) => ListTile(
|
...getIt<AccountsService>().loggedInProfiles.map(
|
||||||
onTap: () async {
|
(p) => ListTile(
|
||||||
await getIt<AccountsService>().setActiveProfile(p);
|
onTap: () async {
|
||||||
if (context.mounted) {
|
await getIt<AccountsService>().setActiveProfile(p);
|
||||||
context.pop();
|
if (context.mounted && context.canPop()) {
|
||||||
}
|
context.pop();
|
||||||
},
|
}
|
||||||
leading: CircleAvatar(
|
},
|
||||||
child: CachedNetworkImage(imageUrl: p.avatar)),
|
leading: CircleAvatar(
|
||||||
title: Text(
|
child: CachedNetworkImage(imageUrl: p.avatar)),
|
||||||
p.username,
|
title: Text(
|
||||||
style: p == getIt<AccountsService>().currentProfile
|
p.username,
|
||||||
? TextStyle(fontWeight: FontWeight.bold)
|
style: p == getIt<AccountsService>().currentProfile
|
||||||
: null,
|
? TextStyle(fontWeight: FontWeight.bold)
|
||||||
),
|
: null,
|
||||||
subtitle: Text(
|
),
|
||||||
p.serverName,
|
subtitle: Text(
|
||||||
style: p == getIt<AccountsService>().currentProfile
|
p.serverName,
|
||||||
? TextStyle(fontWeight: FontWeight.bold)
|
style: p == getIt<AccountsService>().currentProfile
|
||||||
: null,
|
? TextStyle(fontWeight: FontWeight.bold)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
buildMenuButton(
|
||||||
buildMenuButton(
|
context,
|
||||||
context,
|
'Manage Profiles',
|
||||||
'Manage Profiles',
|
() => context.pushNamed(ScreenPaths.manageProfiles),
|
||||||
() => context.pushNamed(ScreenPaths.manageProfiles),
|
),
|
||||||
),
|
const Divider(),
|
||||||
const Divider(),
|
buildMenuButton(
|
||||||
buildMenuButton(
|
context,
|
||||||
context,
|
'Gallery',
|
||||||
'Gallery',
|
() => context.pushNamed(ScreenPaths.gallery),
|
||||||
() => context.pushNamed(ScreenPaths.gallery),
|
),
|
||||||
),
|
buildMenuButton(
|
||||||
buildMenuButton(
|
context,
|
||||||
context,
|
'Direct Messages',
|
||||||
'Direct Messages',
|
() => context.pushNamed(ScreenPaths.messages),
|
||||||
() => context.pushNamed(ScreenPaths.messages),
|
),
|
||||||
),
|
buildMenuButton(
|
||||||
buildMenuButton(
|
context,
|
||||||
context,
|
'Settings',
|
||||||
'Settings',
|
() => context.pushNamed(ScreenPaths.settings),
|
||||||
() => context.pushNamed(ScreenPaths.settings),
|
),
|
||||||
),
|
// TODO Add back in clearing ability but has to do disk caches too
|
||||||
// TODO Add back in clearing ability but has to do disk caches too
|
// buildMenuButton(context, 'Clear Caches', () async {
|
||||||
// buildMenuButton(context, 'Clear Caches', () async {
|
// final confirm = await showYesNoDialog(
|
||||||
// final confirm = await showYesNoDialog(
|
// context, 'You want to clear all memory and disk cache data?');
|
||||||
// context, 'You want to clear all memory and disk cache data?');
|
// if (confirm == true) {
|
||||||
// if (confirm == true) {
|
// clearCaches();
|
||||||
// clearCaches();
|
// }
|
||||||
// }
|
// }),
|
||||||
// }),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,35 +24,45 @@ class TimelinePanel extends StatelessWidget {
|
||||||
return Center(child: Text('Error getting timeline: ${result.error}'));
|
return Center(child: Text('Error getting timeline: ${result.error}'));
|
||||||
}
|
}
|
||||||
final items = result.value;
|
final items = result.value;
|
||||||
return ListView.separated(
|
return RefreshIndicator(
|
||||||
itemBuilder: (context, index) {
|
onRefresh: () async {
|
||||||
if (index == 0) {
|
manager.updateTimeline(
|
||||||
return TextButton(
|
timeline,
|
||||||
onPressed: () async => await manager.updateTimeline(
|
TimelineRefreshType.refresh,
|
||||||
timeline, TimelineRefreshType.loadNewer),
|
|
||||||
child: const Text('Load newer posts'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == items.length + 1) {
|
|
||||||
return TextButton(
|
|
||||||
onPressed: () async => await manager.updateTimeline(
|
|
||||||
timeline, TimelineRefreshType.loadOlder),
|
|
||||||
child: const Text('Load older posts'));
|
|
||||||
}
|
|
||||||
final itemIndex = index - 1;
|
|
||||||
final item = items[itemIndex];
|
|
||||||
_logger
|
|
||||||
.finest('Building item: $itemIndex: ${item.entry.toShortString()}');
|
|
||||||
return PostControl(
|
|
||||||
originalItem: item,
|
|
||||||
scrollToId: item.id,
|
|
||||||
openRemote: false,
|
|
||||||
showStatusOpenButton: true,
|
|
||||||
isRoot: false,
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => Divider(),
|
child: ListView.separated(
|
||||||
itemCount: items.length + 2,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () async => await manager.updateTimeline(
|
||||||
|
timeline, TimelineRefreshType.loadNewer),
|
||||||
|
child: const Text('Load newer posts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == items.length + 1) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () async => await manager.updateTimeline(
|
||||||
|
timeline, TimelineRefreshType.loadOlder),
|
||||||
|
child: const Text('Load older posts'));
|
||||||
|
}
|
||||||
|
final itemIndex = index - 1;
|
||||||
|
final item = items[itemIndex];
|
||||||
|
_logger.finest(
|
||||||
|
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
||||||
|
return PostControl(
|
||||||
|
originalItem: item,
|
||||||
|
scrollToId: item.id,
|
||||||
|
openRemote: false,
|
||||||
|
showStatusOpenButton: true,
|
||||||
|
isRoot: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => Divider(),
|
||||||
|
itemCount: items.length + 2,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,11 @@ class ObjectBoxCache {
|
||||||
final docsDir = await getApplicationSupportDirectory();
|
final docsDir = await getApplicationSupportDirectory();
|
||||||
|
|
||||||
final path = p.join(docsDir.path, baseDir, subDir);
|
final path = p.join(docsDir.path, baseDir, subDir);
|
||||||
Directory(path).createSync(recursive: true);
|
try {
|
||||||
|
Directory(path).createSync(recursive: true);
|
||||||
|
} catch (e) {
|
||||||
|
_logger.severe('Error creating ObjectCachePathDirectory: $e');
|
||||||
|
}
|
||||||
_logger.info('ObjectBoxCache path: $path');
|
_logger.info('ObjectBoxCache path: $path');
|
||||||
final store = await openStore(
|
final store = await openStore(
|
||||||
directory: path, macosApplicationGroup: 'T69YZGT58U.relatica');
|
directory: path, macosApplicationGroup: 'T69YZGT58U.relatica');
|
||||||
|
|
|
@ -87,7 +87,7 @@ Future<void> dependencyInjectionInitialization() async {
|
||||||
Future<void> updateProfileDependencyInjectors(Profile profile) async {
|
Future<void> updateProfileDependencyInjectors(Profile profile) async {
|
||||||
final objectBox = await ObjectBoxCache.create(
|
final objectBox = await ObjectBoxCache.create(
|
||||||
baseDir: 'profileboxcaches',
|
baseDir: 'profileboxcaches',
|
||||||
subDir: '${profile.username}_${profile.serverName}',
|
subDir: '${profile.id}_${profile.serverName}',
|
||||||
);
|
);
|
||||||
final connectionReposSelector =
|
final connectionReposSelector =
|
||||||
getIt<ActiveProfileSelector<IConnectionsRepo>>();
|
getIt<ActiveProfileSelector<IConnectionsRepo>>();
|
||||||
|
|
|
@ -41,8 +41,10 @@ class PagesManager<TResult, TID> {
|
||||||
}
|
}
|
||||||
final result = await onRequest(PagingData(limit: limit));
|
final result = await onRequest(PagingData(limit: limit));
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
final newPage = result.value.map((data) => idMapper(data));
|
if (result.value.previous != null || result.value.next != null) {
|
||||||
_pages.add(newPage);
|
final newPage = result.value.map((data) => idMapper(data));
|
||||||
|
_pages.add(newPage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,7 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../controls/app_bottom_nav_bar.dart';
|
import '../controls/app_bottom_nav_bar.dart';
|
||||||
import '../controls/current_profile_button.dart';
|
import '../controls/current_profile_button.dart';
|
||||||
import '../controls/padding.dart';
|
|
||||||
import '../controls/standard_app_drawer.dart';
|
import '../controls/standard_app_drawer.dart';
|
||||||
import '../controls/status_and_refresh_button.dart';
|
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import '../models/connection.dart';
|
import '../models/connection.dart';
|
||||||
import '../routes.dart';
|
import '../routes.dart';
|
||||||
|
@ -45,10 +43,11 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
);
|
);
|
||||||
late Widget body;
|
late Widget body;
|
||||||
if (contacts.isEmpty) {
|
if (contacts.isEmpty) {
|
||||||
body = const SingleChildScrollView(
|
body = SingleChildScrollView(
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
child: Text('No Contacts'),
|
child: Center(
|
||||||
);
|
child: Text('No contacts'),
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
body = ListView.separated(
|
body = ListView.separated(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
@ -66,67 +65,58 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
separatorBuilder: (context, index) => const Divider(),
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
itemCount: contacts.length);
|
itemCount: contacts.length);
|
||||||
}
|
}
|
||||||
final profileButton = buildCurrentProfileButton(context);
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
drawer: StandardAppDrawer(),
|
child: Scaffold(
|
||||||
body: SafeArea(
|
appBar: AppBar(
|
||||||
child: RefreshIndicator(
|
leading: buildCurrentProfileButton(context),
|
||||||
onRefresh: () async {
|
title: TextField(
|
||||||
if (nss.connectionUpdateStatus.value) {
|
onChanged: (value) {
|
||||||
return;
|
setState(() {
|
||||||
}
|
filterText = value.toLowerCase();
|
||||||
await manager.updateAllContacts();
|
});
|
||||||
},
|
},
|
||||||
child: Column(
|
decoration: InputDecoration(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
labelText: 'Filter By Name',
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
alignLabelWithHint: true,
|
||||||
children: [
|
border: OutlineInputBorder(
|
||||||
Padding(
|
borderSide: BorderSide(
|
||||||
padding: const EdgeInsets.all(8.0),
|
color: Theme.of(context).backgroundColor,
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (profileButton != null) ...[
|
|
||||||
SizedBox(width: 70.0, child: profileButton),
|
|
||||||
const HorizontalPadding(),
|
|
||||||
],
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
filterText = value.toLowerCase();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Filter By Name',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).backgroundColor,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const HorizontalPadding(),
|
|
||||||
StatusAndRefreshButton(
|
|
||||||
valueListenable: nss.connectionUpdateStatus,
|
|
||||||
refreshFunction: () async =>
|
|
||||||
await manager.updateAllContacts(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(5.0),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
),
|
||||||
Expanded(
|
|
||||||
child: body,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
drawer: StandardAppDrawer(),
|
||||||
bottomNavigationBar: AppBottomNavBar(
|
body: SafeArea(
|
||||||
currentButton: NavBarButtons.contacts,
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
if (nss.connectionUpdateStatus.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manager.updateAllContacts();
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: nss.connectionUpdateStatus,
|
||||||
|
builder: (context2, executing, _) {
|
||||||
|
if (executing) {
|
||||||
|
return const LinearProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox();
|
||||||
|
}),
|
||||||
|
Expanded(child: body),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: AppBottomNavBar(
|
||||||
|
currentButton: NavBarButtons.contacts,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:provider/provider.dart';
|
||||||
import '../controls/app_bottom_nav_bar.dart';
|
import '../controls/app_bottom_nav_bar.dart';
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
import '../controls/standard_app_drawer.dart';
|
import '../controls/standard_app_drawer.dart';
|
||||||
import '../controls/status_and_refresh_button.dart';
|
|
||||||
import '../controls/timeline/timeline_panel.dart';
|
import '../controls/timeline/timeline_panel.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import '../models/TimelineIdentifiers.dart';
|
import '../models/TimelineIdentifiers.dart';
|
||||||
|
@ -57,81 +56,95 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
auxData: currentGroup?.id ?? '',
|
auxData: currentGroup?.id ?? '',
|
||||||
);
|
);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: accountService.loggedIn
|
leading: accountService.loggedIn
|
||||||
? Builder(builder: (context) {
|
? Builder(builder: (context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Scaffold.of(context).openDrawer();
|
Scaffold.of(context).openDrawer();
|
||||||
|
},
|
||||||
|
icon: CachedNetworkImage(
|
||||||
|
imageUrl: accountService.currentProfile.avatar));
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
backgroundColor: Theme.of(context).canvasColor,
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (currentType == TimelineType.group)
|
||||||
|
PopupMenuButton<TimelineType>(
|
||||||
|
initialValue: currentType,
|
||||||
|
// Callback that sets the selected popup menu item.
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
currentType = value;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: CachedNetworkImage(
|
itemBuilder: (BuildContext context) => types
|
||||||
imageUrl: accountService.currentProfile.avatar));
|
.map((e) => PopupMenuItem<TimelineType>(
|
||||||
})
|
value: e,
|
||||||
: null,
|
child: Text(e.toLabel()),
|
||||||
backgroundColor: Theme.of(context).canvasColor,
|
))
|
||||||
title: Row(
|
.toList()),
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
if (currentType != TimelineType.group)
|
||||||
children: [
|
DropdownButton<TimelineType>(
|
||||||
DropdownButton<TimelineType>(
|
value: currentType,
|
||||||
value: currentType,
|
items: types
|
||||||
items: types
|
.map((e) => DropdownMenuItem<TimelineType>(
|
||||||
.map((e) => DropdownMenuItem<TimelineType>(
|
value: e,
|
||||||
value: e,
|
child: Text(e.toLabel()),
|
||||||
child: Text(e.toLabel()),
|
))
|
||||||
))
|
.toList(),
|
||||||
.toList(),
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
setState(() {
|
||||||
setState(() {
|
currentType = value!;
|
||||||
currentType = value!;
|
});
|
||||||
});
|
}),
|
||||||
}),
|
const HorizontalPadding(
|
||||||
const HorizontalPadding(),
|
width: 5.0,
|
||||||
if (currentType == TimelineType.group)
|
),
|
||||||
DropdownButton<GroupData>(
|
if (currentType == TimelineType.group)
|
||||||
value: currentGroup,
|
DropdownButton<GroupData>(
|
||||||
items: groups
|
value: currentGroup,
|
||||||
.map((g) => DropdownMenuItem<GroupData>(
|
items: groups
|
||||||
value: g,
|
.map((g) => DropdownMenuItem<GroupData>(
|
||||||
child: Text(g.name),
|
value: g,
|
||||||
))
|
child: Text(g.name),
|
||||||
.toList(),
|
))
|
||||||
onChanged: (value) {
|
.toList(),
|
||||||
setState(() {
|
onChanged: (value) {
|
||||||
currentGroup = value;
|
setState(() {
|
||||||
});
|
currentGroup = value;
|
||||||
}),
|
});
|
||||||
],
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
body: Center(
|
||||||
StatusAndRefreshButton(
|
child: Column(
|
||||||
valueListenable: nss.timelineLoadingStatus,
|
children: [
|
||||||
refreshFunction: () async => await manager.updateTimeline(
|
ValueListenableBuilder(
|
||||||
currentTimeline, TimelineRefreshType.refresh),
|
valueListenable: nss.timelineLoadingStatus,
|
||||||
buttonColor: Theme.of(context).textTheme.bodyLarge?.color,
|
builder: (context2, executing, _) {
|
||||||
|
if (executing) {
|
||||||
|
return const LinearProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox();
|
||||||
|
}),
|
||||||
|
Expanded(child: TimelinePanel(timeline: currentTimeline)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
),
|
||||||
onPressed: () {
|
drawer: StandardAppDrawer(),
|
||||||
context.push('/post/new');
|
bottomNavigationBar: const AppBottomNavBar(
|
||||||
},
|
currentButton: NavBarButtons.timelines,
|
||||||
icon: Icon(
|
),
|
||||||
Icons.edit,
|
floatingActionButton: FloatingActionButton.small(
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
onPressed: () {
|
||||||
),
|
context.push('/post/new');
|
||||||
),
|
},
|
||||||
],
|
child: Icon(Icons.add),
|
||||||
),
|
));
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TimelinePanel(
|
|
||||||
timeline: currentTimeline,
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer: StandardAppDrawer(),
|
|
||||||
bottomNavigationBar: const AppBottomNavBar(
|
|
||||||
currentButton: NavBarButtons.timelines,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ class NotificationsScreen extends StatelessWidget {
|
||||||
StatusAndRefreshButton(
|
StatusAndRefreshButton(
|
||||||
valueListenable: nss.notificationsUpdateStatus,
|
valueListenable: nss.notificationsUpdateStatus,
|
||||||
refreshFunction: () async => manager.updateNotifications(),
|
refreshFunction: () async => manager.updateNotifications(),
|
||||||
busyColor: Theme.of(context).colorScheme.background,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async => _clearAllNotifications(context, manager),
|
onPressed: () async => _clearAllNotifications(context, manager),
|
||||||
|
@ -42,7 +41,6 @@ class NotificationsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (notifications.isEmpty) {
|
if (notifications.isEmpty) {
|
||||||
manager.updateNotifications();
|
|
||||||
title = 'Notifications';
|
title = 'Notifications';
|
||||||
body = Center(
|
body = Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -52,12 +50,15 @@ class NotificationsScreen extends StatelessWidget {
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
final unreadCount = notifications.where((e) => !e.dismissed).length;
|
final unreadCount = notifications.where((e) => !e.dismissed).length;
|
||||||
title = 'Notifications ($unreadCount)';
|
title =
|
||||||
|
'Notifications'; //TODO wire in the summary count data if has that endpoint
|
||||||
body = RefreshIndicator(
|
body = RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
manager.updateNotifications();
|
manager.updateNotifications();
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
|
|
|
@ -136,6 +136,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
autocorrect: false,
|
||||||
readOnly: existingAccount,
|
readOnly: existingAccount,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
controller: serverNameController,
|
controller: serverNameController,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:relatica/utils/active_profile_selector.dart';
|
||||||
|
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
|
@ -40,7 +41,10 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final manager = context.watch<ConnectionsManager>();
|
final manager = context
|
||||||
|
.watch<ActiveProfileSelector<ConnectionsManager>>()
|
||||||
|
.activeEntry
|
||||||
|
.value;
|
||||||
final body = manager.getById(widget.userId).fold(onSuccess: (profile) {
|
final body = manager.getById(widget.userId).fold(onSuccess: (profile) {
|
||||||
final notMyProfile =
|
final notMyProfile =
|
||||||
getIt<AccountsService>().currentProfile.userId != profile.id;
|
getIt<AccountsService>().currentProfile.userId != profile.id;
|
||||||
|
|
|
@ -14,10 +14,12 @@ import 'auth_service.dart';
|
||||||
class DirectMessageService extends ChangeNotifier {
|
class DirectMessageService extends ChangeNotifier {
|
||||||
static final _logger = Logger('$DirectMessageService');
|
static final _logger = Logger('$DirectMessageService');
|
||||||
final _threads = <String, DirectMessageThread>{};
|
final _threads = <String, DirectMessageThread>{};
|
||||||
|
var _firstLoading = true;
|
||||||
|
|
||||||
List<DirectMessageThread> getThreads({bool unreadyOnly = false}) {
|
List<DirectMessageThread> getThreads({bool unreadyOnly = false}) {
|
||||||
if (_threads.isEmpty) {
|
if (_threads.isEmpty && _firstLoading) {
|
||||||
updateThreads();
|
updateThreads();
|
||||||
|
_firstLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unreadyOnly) {
|
if (unreadyOnly) {
|
||||||
|
|
|
@ -68,8 +68,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
|
|
||||||
FutureResult<bool, ExecError> deleteEntryById(String id) async {
|
FutureResult<bool, ExecError> deleteEntryById(String id) async {
|
||||||
_logger.finest('Delete entry: $id');
|
_logger.finest('Delete entry: $id');
|
||||||
final result =
|
final result = await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||||
await StatusesClient(getIt<AccountsService>().currentProfile)
|
|
||||||
.deleteEntryById(id);
|
.deleteEntryById(id);
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
return result.errorCast();
|
return result.errorCast();
|
||||||
|
@ -80,7 +79,8 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
return Result.ok(true);
|
return Result.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<bool, ExecError> createNewStatus(String text, {
|
FutureResult<bool, ExecError> createNewStatus(
|
||||||
|
String text, {
|
||||||
String spoilerText = '',
|
String spoilerText = '',
|
||||||
String inReplyToId = '',
|
String inReplyToId = '',
|
||||||
required NewEntryMediaItems mediaItems,
|
required NewEntryMediaItems mediaItems,
|
||||||
|
@ -108,12 +108,12 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
final uploadResult =
|
final uploadResult =
|
||||||
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
||||||
item.localFilePath,
|
item.localFilePath,
|
||||||
).andThenAsync(
|
).andThenAsync(
|
||||||
(imageBytes) async =>
|
(imageBytes) async =>
|
||||||
await RemoteFileClient(getIt<AccountsService>().currentProfile)
|
await RemoteFileClient(getIt<AccountsService>().currentProfile)
|
||||||
.uploadFileAsAttachment(
|
.uploadFileAsAttachment(
|
||||||
bytes: imageBytes,
|
bytes: imageBytes,
|
||||||
album: mediaItems.albumName,
|
album: mediaItems.albumName,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
|
@ -129,13 +129,12 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final result =
|
final result = await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||||
await StatusesClient(getIt<AccountsService>().currentProfile)
|
|
||||||
.createNewStatus(
|
.createNewStatus(
|
||||||
text: text,
|
text: text,
|
||||||
spoilerText: spoilerText,
|
spoilerText: spoilerText,
|
||||||
inReplyToId: inReplyToId,
|
inReplyToId: inReplyToId,
|
||||||
mediaIds: mediaIds)
|
mediaIds: mediaIds)
|
||||||
.andThenSuccessAsync((item) async {
|
.andThenSuccessAsync((item) async {
|
||||||
await processNewItems(
|
await processNewItems(
|
||||||
[item], getIt<AccountsService>().currentProfile.username, null);
|
[item], getIt<AccountsService>().currentProfile.username, null);
|
||||||
|
@ -157,7 +156,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
_logger.finest('${status.id} status created');
|
_logger.finest('${status.id} status created');
|
||||||
return true;
|
return true;
|
||||||
}).mapError(
|
}).mapError(
|
||||||
(error) {
|
(error) {
|
||||||
_logger.finest('Error creating post: $error');
|
_logger.finest('Error creating post: $error');
|
||||||
return ExecError(
|
return ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
|
@ -184,12 +183,10 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
|
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
|
||||||
final updatedPosts = await processNewItems(
|
final updatedPosts =
|
||||||
itemsResult.value, client.profile.userId, client);
|
await processNewItems(itemsResult.value, client.profile.userId, client);
|
||||||
_logger.finest(() {
|
_logger.finest(() {
|
||||||
final postCount = _entries.values
|
final postCount = _entries.values.where((e) => e.parentId.isEmpty).length;
|
||||||
.where((e) => e.parentId.isEmpty)
|
|
||||||
.length;
|
|
||||||
final commentCount = _entries.length - postCount;
|
final commentCount = _entries.length - postCount;
|
||||||
final orphanCount = _entries.values
|
final orphanCount = _entries.values
|
||||||
.where(
|
.where(
|
||||||
|
@ -200,9 +197,11 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
return Result.ok(updatedPosts);
|
return Result.ok(updatedPosts);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<EntryTreeItem>> processNewItems(List<TimelineEntry> items,
|
Future<List<EntryTreeItem>> processNewItems(
|
||||||
String currentId,
|
List<TimelineEntry> items,
|
||||||
FriendicaClient? client,) async {
|
String currentId,
|
||||||
|
FriendicaClient? client,
|
||||||
|
) async {
|
||||||
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
||||||
final allSeenItems = [...items];
|
final allSeenItems = [...items];
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
|
@ -229,9 +228,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
await StatusesClient(getIt<AccountsService>().currentProfile)
|
await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||||
.getPostOrComment(o.id, fullContext: true)
|
.getPostOrComment(o.id, fullContext: true)
|
||||||
.andThenSuccessAsync((items) async {
|
.andThenSuccessAsync((items) async {
|
||||||
final parentPostId = items
|
final parentPostId = items.firstWhere((e) => e.parentId.isEmpty).id;
|
||||||
.firstWhere((e) => e.parentId.isEmpty)
|
|
||||||
.id;
|
|
||||||
_parentPostIds[o.id] = parentPostId;
|
_parentPostIds[o.id] = parentPostId;
|
||||||
allSeenItems.addAll(items);
|
allSeenItems.addAll(items);
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
|
@ -259,7 +256,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
for (final item in seenItemsCopy) {
|
for (final item in seenItemsCopy) {
|
||||||
if (item.parentId.isEmpty) {
|
if (item.parentId.isEmpty) {
|
||||||
final postNode =
|
final postNode =
|
||||||
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
||||||
postNodesToReturn.add(postNode);
|
postNodesToReturn.add(postNode);
|
||||||
allSeenItems.remove(item);
|
allSeenItems.remove(item);
|
||||||
} else {
|
} else {
|
||||||
|
@ -301,9 +298,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
_logger.finest(
|
_logger.finest(
|
||||||
'Completed processing new items ${client == null
|
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
|
||||||
? 'sub level'
|
|
||||||
: 'top level'}');
|
|
||||||
return updatedPosts;
|
return updatedPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,11 +307,10 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
||||||
final result = await client
|
final result = await client
|
||||||
.getPostOrComment(id, fullContext: false)
|
.getPostOrComment(id, fullContext: false)
|
||||||
.andThenAsync((rootItems) async =>
|
.andThenAsync((rootItems) async => await client
|
||||||
await client
|
.getPostOrComment(id, fullContext: true)
|
||||||
.getPostOrComment(id, fullContext: true)
|
.andThenSuccessAsync(
|
||||||
.andThenSuccessAsync(
|
(contextItems) async => [...rootItems, ...contextItems]))
|
||||||
(contextItems) async => [...rootItems, ...contextItems]))
|
|
||||||
.andThenSuccessAsync((items) async {
|
.andThenSuccessAsync((items) async {
|
||||||
await processNewItems(items, client.profile.username, null);
|
await processNewItems(items, client.profile.username, null);
|
||||||
});
|
});
|
||||||
|
@ -325,7 +319,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
_logger.finest('$id post updated');
|
_logger.finest('$id post updated');
|
||||||
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
|
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
|
||||||
}).mapError(
|
}).mapError(
|
||||||
(error) {
|
(error) {
|
||||||
_logger.finest('$id error updating: $error');
|
_logger.finest('$id error updating: $error');
|
||||||
return ExecError(
|
return ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
|
@ -339,7 +333,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
_logger.finest('Resharing post: $id');
|
_logger.finest('Resharing post: $id');
|
||||||
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
||||||
final result =
|
final result =
|
||||||
await client.resharePost(id).andThenSuccessAsync((item) async {
|
await client.resharePost(id).andThenSuccessAsync((item) async {
|
||||||
await processNewItems([item], client.profile.username, null);
|
await processNewItems([item], client.profile.username, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -347,7 +341,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
_logger.finest('$id post updated after reshare');
|
_logger.finest('$id post updated after reshare');
|
||||||
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
|
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
|
||||||
}).mapError(
|
}).mapError(
|
||||||
(error) {
|
(error) {
|
||||||
_logger.finest('$id error updating: $error');
|
_logger.finest('$id error updating: $error');
|
||||||
return ExecError(
|
return ExecError(
|
||||||
type: ErrorType.localError,
|
type: ErrorType.localError,
|
||||||
|
@ -361,7 +355,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
_logger.finest('Unresharing post: $id');
|
_logger.finest('Unresharing post: $id');
|
||||||
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
||||||
final result =
|
final result =
|
||||||
await client.unResharePost(id).andThenSuccessAsync((item) async {
|
await client.unResharePost(id).andThenSuccessAsync((item) async {
|
||||||
await processNewItems([item], client.profile.username, null);
|
await processNewItems([item], client.profile.username, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -375,10 +369,9 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
return Result.ok(true);
|
return Result.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(String id,
|
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||||
bool newStatus) async {
|
String id, bool newStatus) async {
|
||||||
final client =
|
final client = InteractionsClient(getIt<AccountsService>().currentProfile);
|
||||||
InteractionsClient(getIt<AccountsService>().currentProfile);
|
|
||||||
final result = await client.changeFavoriteStatus(id, newStatus);
|
final result = await client.changeFavoriteStatus(id, newStatus);
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
return result.errorCast();
|
return result.errorCast();
|
||||||
|
@ -388,7 +381,7 @@ class EntryManagerService extends ChangeNotifier {
|
||||||
_entries[update.id] = update;
|
_entries[update.id] = update;
|
||||||
final node = update.parentId.isEmpty
|
final node = update.parentId.isEmpty
|
||||||
? _postNodes[update.id]!
|
? _postNodes[update.id]!
|
||||||
: _postNodes[update.parentId]!.getChildById(update.id)!;
|
: _postNodes[_parentPostIds[update.parentId]]!;
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return Result.ok(_nodeToTreeItem(node, client.profile.userId));
|
return Result.ok(_nodeToTreeItem(node, client.profile.userId));
|
||||||
|
@ -468,7 +461,7 @@ class _Node {
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
int get hashCode => id.hashCode;
|
||||||
|
|
|
@ -22,8 +22,13 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
idMapper: (nn) => nn.map((n) => n.id).toList(),
|
idMapper: (nn) => nn.map((n) => n.id).toList(),
|
||||||
onRequest: _clientGetNotificationsRequest,
|
onRequest: _clientGetNotificationsRequest,
|
||||||
);
|
);
|
||||||
|
var _firstLoad = true;
|
||||||
|
|
||||||
List<UserNotification> get notifications {
|
List<UserNotification> get notifications {
|
||||||
|
if (_notifications.isEmpty && _firstLoad) {
|
||||||
|
updateNotifications();
|
||||||
|
_firstLoad = false;
|
||||||
|
}
|
||||||
final result = List<UserNotification>.from(_notifications.values);
|
final result = List<UserNotification>.from(_notifications.values);
|
||||||
result.sort((n1, n2) {
|
result.sort((n1, n2) {
|
||||||
if (n1.dismissed == n2.dismissed) {
|
if (n1.dismissed == n2.dismissed) {
|
||||||
|
@ -46,7 +51,7 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
||||||
const initialPull = 25;
|
const initialPull = 100;
|
||||||
final nn = <UserNotification>[];
|
final nn = <UserNotification>[];
|
||||||
if (_pm.pages.isEmpty) {
|
if (_pm.pages.isEmpty) {
|
||||||
final result = await _pm.initialize(initialPull);
|
final result = await _pm.initialize(initialPull);
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: relatica
|
||||||
description: A mobile and desktop client for interacting with the Friendica social network
|
description: A mobile and desktop client for interacting with the Friendica social network
|
||||||
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 0.1.0+1
|
version: 0.2.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
|
|
BIN
screenshots/v0.2.0/linux/relatica_v0.2.0_drawer.png
Executable file
BIN
screenshots/v0.2.0/linux/relatica_v0.2.0_drawer.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
BIN
screenshots/v0.2.0/linux/relatica_v0.2.0_home.png
Executable file
BIN
screenshots/v0.2.0/linux/relatica_v0.2.0_home.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
Loading…
Reference in a new issue