mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 15:53: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
|
||||
|
||||
## 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
|
||||
* The timeline names have been changed to:
|
||||
|
@ -17,6 +44,8 @@
|
|||
* Keyboard type on login screen for username field is now "email" type
|
||||
* New Features
|
||||
* 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)
|
||||
* 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
|
||||
|
@ -25,7 +54,7 @@
|
|||
* 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
|
||||
|
||||
## Version 0.1.0b2 (beta)
|
||||
## Version 0.1.0b2 (beta), 27 January 2023
|
||||
|
||||
* Fixes
|
||||
* 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
|
||||
posts/comments"
|
||||
|
||||
## Version 0.1.0 (beta)
|
||||
## Version 0.1.0 (beta), 20 January 2023
|
||||
|
||||
* Initial public release, as an early beta.
|
||||
* Major working features (at least in initial implementation versions):
|
||||
|
|
15
README.md
15
README.md
|
@ -2,27 +2,28 @@
|
|||
|
||||
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
|
||||
|
||||
* 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 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
|
||||
|
||||
|
||||
## Current Status
|
||||
|
||||
The project is currently in an early-beta state. If you'd like to use it at this time please
|
||||
see the notes at [Relatica Beta Testing Program](beta-program.md).
|
||||
The project is currently in an early-beta state. If you'd like to use it at this time please
|
||||
see the notes at [Relatica Beta Testing Program](beta-program.md).
|
||||
|
||||
It is possible to install it now by following the [install instructions](install.md).
|
||||
|
||||
If you would like to contribute please see [this Developers Notes](developers.md) section.
|
||||
|
||||
|
||||
## Community and Support
|
||||
|
||||
[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)
|
||||
|
||||
### Things I could use help from community on:
|
||||
|
||||
* More coders and testers are always welcome
|
||||
* CI/CD
|
||||
* 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
|
||||
|
||||
## 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
|
||||
|
||||
* 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
|
||||
* 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)
|
||||
|
@ -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)
|
||||
* Refresh notifications and contacts gets updates to that respective data (this may
|
||||
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:
|
||||
|
||||
* Show list of who liked and reshared posts/comments
|
||||
* More timeline types like Comments, "By Activity", etc.
|
||||
* 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:
|
||||
* Allowing images/thumbnails to be larger
|
||||
* 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
|
||||
* User configurable Server blocking
|
||||
* Server-side searching tied into profiles, posts, hashtags
|
||||
* OAuth logins
|
||||
* Being able to ignore/unignore users
|
||||
* Deleting images and entire galleries
|
||||
* Events
|
||||
|
@ -134,7 +135,6 @@ wanted to lay out some expectations before getting into the small details
|
|||
* Account creation through the application
|
||||
* Creating new forums through the application
|
||||
* Site administration
|
||||
* Two-factor authentication
|
||||
* Adding videos or files to posts
|
||||
* 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:
|
||||
|
||||
* 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
|
||||
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
|
||||
Diaspora for the time being
|
||||
* 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
|
||||
side. That includes things like:
|
||||
* 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
|
||||
through the app will have this privacy level.
|
||||
* 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
|
||||
muddled display. Restarting the app fixes this.
|
||||
* Some images within posts, usually graphical emojis, are rendered drastically larger than they
|
||||
should be.
|
||||
* On Linux you will need to re-enter your credentials each time you use the app for the first time
|
||||
after logging in.
|
||||
* 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.
|
||||
* On Linux you will need to login to the key manager and unlock it before opening the app. Some Linux versions do this
|
||||
automatically while others do not.
|
||||
* 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:
|
||||
```
|
||||
|
|
11
install.md
11
install.md
|
@ -6,12 +6,11 @@ For more information about the current beta testing program
|
|||
|
||||
# Latest Binaries:
|
||||
|
||||
* [Android v0.1.0b3](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.1.0/relatica_v0.1.0b3.apk.zip)
|
||||
* iPhone/iPad v0.1.0b3: 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)
|
||||
* [macOS v0.1.0b3](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.1.0/Relatica_v0.1.0b3_mac.zip)
|
||||
also through TestFlight
|
||||
* [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)
|
||||
* [Android v0.2.0](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.2.0%2Frelatica_v0.2.0.apk.zip)
|
||||
* iPhone/iPad v0.2.0: This is only available through TestFlight. Please contact me for access.
|
||||
* [Windows (Intel) v0.2.0](https://mysocialportal-relatica.nyc3.cdn.digitaloceanspaces.com/v0.2.0%2FRelatica_v0.2.0_x64_win.zip)
|
||||
* macOS v0.2.0 This is only available through TestFlight. Please contact me for access.
|
||||
* [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)
|
||||
|
||||
## Mobile
|
||||
|
||||
|
|
|
@ -42,12 +42,7 @@ class AppBottomNavBar extends StatelessWidget {
|
|||
|
||||
switch (newButton) {
|
||||
case NavBarButtons.timelines:
|
||||
try {
|
||||
Navigator.of(context)
|
||||
.popUntil(ModalRoute.withName(ScreenPaths.timelines));
|
||||
} catch (e) {
|
||||
context.go(ScreenPaths.timelines);
|
||||
}
|
||||
context.go(ScreenPaths.timelines);
|
||||
break;
|
||||
case NavBarButtons.notifications:
|
||||
context.pushNamed(ScreenPaths.notifications);
|
||||
|
|
|
@ -9,64 +9,66 @@ import '../services/auth_service.dart';
|
|||
class StandardAppDrawer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
...getIt<AccountsService>().loggedInProfiles.map(
|
||||
(p) => ListTile(
|
||||
onTap: () async {
|
||||
await getIt<AccountsService>().setActiveProfile(p);
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
child: CachedNetworkImage(imageUrl: p.avatar)),
|
||||
title: Text(
|
||||
p.username,
|
||||
style: p == getIt<AccountsService>().currentProfile
|
||||
? TextStyle(fontWeight: FontWeight.bold)
|
||||
: null,
|
||||
),
|
||||
subtitle: Text(
|
||||
p.serverName,
|
||||
style: p == getIt<AccountsService>().currentProfile
|
||||
? TextStyle(fontWeight: FontWeight.bold)
|
||||
: null,
|
||||
return SafeArea(
|
||||
child: Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
...getIt<AccountsService>().loggedInProfiles.map(
|
||||
(p) => ListTile(
|
||||
onTap: () async {
|
||||
await getIt<AccountsService>().setActiveProfile(p);
|
||||
if (context.mounted && context.canPop()) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
child: CachedNetworkImage(imageUrl: p.avatar)),
|
||||
title: Text(
|
||||
p.username,
|
||||
style: p == getIt<AccountsService>().currentProfile
|
||||
? TextStyle(fontWeight: FontWeight.bold)
|
||||
: null,
|
||||
),
|
||||
subtitle: Text(
|
||||
p.serverName,
|
||||
style: p == getIt<AccountsService>().currentProfile
|
||||
? TextStyle(fontWeight: FontWeight.bold)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Manage Profiles',
|
||||
() => context.pushNamed(ScreenPaths.manageProfiles),
|
||||
),
|
||||
const Divider(),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Gallery',
|
||||
() => context.pushNamed(ScreenPaths.gallery),
|
||||
),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Direct Messages',
|
||||
() => context.pushNamed(ScreenPaths.messages),
|
||||
),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Settings',
|
||||
() => context.pushNamed(ScreenPaths.settings),
|
||||
),
|
||||
// TODO Add back in clearing ability but has to do disk caches too
|
||||
// buildMenuButton(context, 'Clear Caches', () async {
|
||||
// final confirm = await showYesNoDialog(
|
||||
// context, 'You want to clear all memory and disk cache data?');
|
||||
// if (confirm == true) {
|
||||
// clearCaches();
|
||||
// }
|
||||
// }),
|
||||
],
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Manage Profiles',
|
||||
() => context.pushNamed(ScreenPaths.manageProfiles),
|
||||
),
|
||||
const Divider(),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Gallery',
|
||||
() => context.pushNamed(ScreenPaths.gallery),
|
||||
),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Direct Messages',
|
||||
() => context.pushNamed(ScreenPaths.messages),
|
||||
),
|
||||
buildMenuButton(
|
||||
context,
|
||||
'Settings',
|
||||
() => context.pushNamed(ScreenPaths.settings),
|
||||
),
|
||||
// TODO Add back in clearing ability but has to do disk caches too
|
||||
// buildMenuButton(context, 'Clear Caches', () async {
|
||||
// final confirm = await showYesNoDialog(
|
||||
// context, 'You want to clear all memory and disk cache data?');
|
||||
// if (confirm == true) {
|
||||
// clearCaches();
|
||||
// }
|
||||
// }),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,35 +24,45 @@ class TimelinePanel extends StatelessWidget {
|
|||
return Center(child: Text('Error getting timeline: ${result.error}'));
|
||||
}
|
||||
final items = result.value;
|
||||
return ListView.separated(
|
||||
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,
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
manager.updateTimeline(
|
||||
timeline,
|
||||
TimelineRefreshType.refresh,
|
||||
);
|
||||
return;
|
||||
},
|
||||
separatorBuilder: (context, index) => Divider(),
|
||||
itemCount: items.length + 2,
|
||||
child: ListView.separated(
|
||||
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 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');
|
||||
final store = await openStore(
|
||||
directory: path, macosApplicationGroup: 'T69YZGT58U.relatica');
|
||||
|
|
|
@ -87,7 +87,7 @@ Future<void> dependencyInjectionInitialization() async {
|
|||
Future<void> updateProfileDependencyInjectors(Profile profile) async {
|
||||
final objectBox = await ObjectBoxCache.create(
|
||||
baseDir: 'profileboxcaches',
|
||||
subDir: '${profile.username}_${profile.serverName}',
|
||||
subDir: '${profile.id}_${profile.serverName}',
|
||||
);
|
||||
final connectionReposSelector =
|
||||
getIt<ActiveProfileSelector<IConnectionsRepo>>();
|
||||
|
|
|
@ -41,8 +41,10 @@ class PagesManager<TResult, TID> {
|
|||
}
|
||||
final result = await onRequest(PagingData(limit: limit));
|
||||
if (result.isSuccess) {
|
||||
final newPage = result.value.map((data) => idMapper(data));
|
||||
_pages.add(newPage);
|
||||
if (result.value.previous != null || result.value.next != null) {
|
||||
final newPage = result.value.map((data) => idMapper(data));
|
||||
_pages.add(newPage);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
import '../controls/app_bottom_nav_bar.dart';
|
||||
import '../controls/current_profile_button.dart';
|
||||
import '../controls/padding.dart';
|
||||
import '../controls/standard_app_drawer.dart';
|
||||
import '../controls/status_and_refresh_button.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/connection.dart';
|
||||
import '../routes.dart';
|
||||
|
@ -45,10 +43,11 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
|||
);
|
||||
late Widget body;
|
||||
if (contacts.isEmpty) {
|
||||
body = const SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Text('No Contacts'),
|
||||
);
|
||||
body = SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Center(
|
||||
child: Text('No contacts'),
|
||||
));
|
||||
} else {
|
||||
body = ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
|
@ -66,67 +65,58 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
|||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemCount: contacts.length);
|
||||
}
|
||||
final profileButton = buildCurrentProfileButton(context);
|
||||
return Scaffold(
|
||||
drawer: StandardAppDrawer(),
|
||||
body: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (nss.connectionUpdateStatus.value) {
|
||||
return;
|
||||
}
|
||||
await manager.updateAllContacts();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
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(),
|
||||
)
|
||||
],
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: buildCurrentProfileButton(context),
|
||||
title: 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 VerticalPadding(),
|
||||
Expanded(
|
||||
child: body,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: AppBottomNavBar(
|
||||
currentButton: NavBarButtons.contacts,
|
||||
drawer: StandardAppDrawer(),
|
||||
body: SafeArea(
|
||||
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/padding.dart';
|
||||
import '../controls/standard_app_drawer.dart';
|
||||
import '../controls/status_and_refresh_button.dart';
|
||||
import '../controls/timeline/timeline_panel.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/TimelineIdentifiers.dart';
|
||||
|
@ -57,81 +56,95 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
auxData: currentGroup?.id ?? '',
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: accountService.loggedIn
|
||||
? Builder(builder: (context) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
appBar: AppBar(
|
||||
leading: accountService.loggedIn
|
||||
? Builder(builder: (context) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
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(
|
||||
imageUrl: accountService.currentProfile.avatar));
|
||||
})
|
||||
: null,
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButton<TimelineType>(
|
||||
value: currentType,
|
||||
items: types
|
||||
.map((e) => DropdownMenuItem<TimelineType>(
|
||||
value: e,
|
||||
child: Text(e.toLabel()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
currentType = value!;
|
||||
});
|
||||
}),
|
||||
const HorizontalPadding(),
|
||||
if (currentType == TimelineType.group)
|
||||
DropdownButton<GroupData>(
|
||||
value: currentGroup,
|
||||
items: groups
|
||||
.map((g) => DropdownMenuItem<GroupData>(
|
||||
value: g,
|
||||
child: Text(g.name),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
currentGroup = value;
|
||||
});
|
||||
}),
|
||||
],
|
||||
itemBuilder: (BuildContext context) => types
|
||||
.map((e) => PopupMenuItem<TimelineType>(
|
||||
value: e,
|
||||
child: Text(e.toLabel()),
|
||||
))
|
||||
.toList()),
|
||||
if (currentType != TimelineType.group)
|
||||
DropdownButton<TimelineType>(
|
||||
value: currentType,
|
||||
items: types
|
||||
.map((e) => DropdownMenuItem<TimelineType>(
|
||||
value: e,
|
||||
child: Text(e.toLabel()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
currentType = value!;
|
||||
});
|
||||
}),
|
||||
const HorizontalPadding(
|
||||
width: 5.0,
|
||||
),
|
||||
if (currentType == TimelineType.group)
|
||||
DropdownButton<GroupData>(
|
||||
value: currentGroup,
|
||||
items: groups
|
||||
.map((g) => DropdownMenuItem<GroupData>(
|
||||
value: g,
|
||||
child: Text(g.name),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
currentGroup = value;
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
StatusAndRefreshButton(
|
||||
valueListenable: nss.timelineLoadingStatus,
|
||||
refreshFunction: () async => await manager.updateTimeline(
|
||||
currentTimeline, TimelineRefreshType.refresh),
|
||||
buttonColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: nss.timelineLoadingStatus,
|
||||
builder: (context2, executing, _) {
|
||||
if (executing) {
|
||||
return const LinearProgressIndicator();
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}),
|
||||
Expanded(child: TimelinePanel(timeline: currentTimeline)),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.push('/post/new');
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TimelinePanel(
|
||||
timeline: currentTimeline,
|
||||
)),
|
||||
],
|
||||
),
|
||||
drawer: StandardAppDrawer(),
|
||||
bottomNavigationBar: const AppBottomNavBar(
|
||||
currentButton: NavBarButtons.timelines,
|
||||
),
|
||||
);
|
||||
),
|
||||
drawer: StandardAppDrawer(),
|
||||
bottomNavigationBar: const AppBottomNavBar(
|
||||
currentButton: NavBarButtons.timelines,
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.small(
|
||||
onPressed: () {
|
||||
context.push('/post/new');
|
||||
},
|
||||
child: Icon(Icons.add),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ class NotificationsScreen extends StatelessWidget {
|
|||
StatusAndRefreshButton(
|
||||
valueListenable: nss.notificationsUpdateStatus,
|
||||
refreshFunction: () async => manager.updateNotifications(),
|
||||
busyColor: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async => _clearAllNotifications(context, manager),
|
||||
|
@ -42,7 +41,6 @@ class NotificationsScreen extends StatelessWidget {
|
|||
),
|
||||
];
|
||||
if (notifications.isEmpty) {
|
||||
manager.updateNotifications();
|
||||
title = 'Notifications';
|
||||
body = Center(
|
||||
child: Column(
|
||||
|
@ -52,12 +50,15 @@ class NotificationsScreen extends StatelessWidget {
|
|||
));
|
||||
} else {
|
||||
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(
|
||||
onRefresh: () async {
|
||||
manager.updateNotifications();
|
||||
return;
|
||||
},
|
||||
child: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return TextButton(
|
||||
|
|
|
@ -136,6 +136,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
),
|
||||
const VerticalPadding(),
|
||||
TextFormField(
|
||||
autocorrect: false,
|
||||
readOnly: existingAccount,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
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:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relatica/utils/active_profile_selector.dart';
|
||||
|
||||
import '../controls/padding.dart';
|
||||
import '../globals.dart';
|
||||
|
@ -40,7 +41,10 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
|||
|
||||
@override
|
||||
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 notMyProfile =
|
||||
getIt<AccountsService>().currentProfile.userId != profile.id;
|
||||
|
|
|
@ -14,10 +14,12 @@ import 'auth_service.dart';
|
|||
class DirectMessageService extends ChangeNotifier {
|
||||
static final _logger = Logger('$DirectMessageService');
|
||||
final _threads = <String, DirectMessageThread>{};
|
||||
var _firstLoading = true;
|
||||
|
||||
List<DirectMessageThread> getThreads({bool unreadyOnly = false}) {
|
||||
if (_threads.isEmpty) {
|
||||
if (_threads.isEmpty && _firstLoading) {
|
||||
updateThreads();
|
||||
_firstLoading = false;
|
||||
}
|
||||
|
||||
if (unreadyOnly) {
|
||||
|
|
|
@ -68,8 +68,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
|
||||
FutureResult<bool, ExecError> deleteEntryById(String id) async {
|
||||
_logger.finest('Delete entry: $id');
|
||||
final result =
|
||||
await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||
final result = await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||
.deleteEntryById(id);
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
|
@ -80,7 +79,8 @@ class EntryManagerService extends ChangeNotifier {
|
|||
return Result.ok(true);
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> createNewStatus(String text, {
|
||||
FutureResult<bool, ExecError> createNewStatus(
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
|
@ -108,12 +108,12 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
|
||||
final uploadResult =
|
||||
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
||||
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
||||
item.localFilePath,
|
||||
).andThenAsync(
|
||||
(imageBytes) async =>
|
||||
await RemoteFileClient(getIt<AccountsService>().currentProfile)
|
||||
.uploadFileAsAttachment(
|
||||
(imageBytes) async =>
|
||||
await RemoteFileClient(getIt<AccountsService>().currentProfile)
|
||||
.uploadFileAsAttachment(
|
||||
bytes: imageBytes,
|
||||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
|
@ -129,13 +129,12 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
final result =
|
||||
await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||
final result = await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||
.createNewStatus(
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaIds: mediaIds)
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaIds: mediaIds)
|
||||
.andThenSuccessAsync((item) async {
|
||||
await processNewItems(
|
||||
[item], getIt<AccountsService>().currentProfile.username, null);
|
||||
|
@ -157,7 +156,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_logger.finest('${status.id} status created');
|
||||
return true;
|
||||
}).mapError(
|
||||
(error) {
|
||||
(error) {
|
||||
_logger.finest('Error creating post: $error');
|
||||
return ExecError(
|
||||
type: ErrorType.localError,
|
||||
|
@ -184,12 +183,10 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
|
||||
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
|
||||
final updatedPosts = await processNewItems(
|
||||
itemsResult.value, client.profile.userId, client);
|
||||
final updatedPosts =
|
||||
await processNewItems(itemsResult.value, client.profile.userId, client);
|
||||
_logger.finest(() {
|
||||
final postCount = _entries.values
|
||||
.where((e) => e.parentId.isEmpty)
|
||||
.length;
|
||||
final postCount = _entries.values.where((e) => e.parentId.isEmpty).length;
|
||||
final commentCount = _entries.length - postCount;
|
||||
final orphanCount = _entries.values
|
||||
.where(
|
||||
|
@ -200,9 +197,11 @@ class EntryManagerService extends ChangeNotifier {
|
|||
return Result.ok(updatedPosts);
|
||||
}
|
||||
|
||||
Future<List<EntryTreeItem>> processNewItems(List<TimelineEntry> items,
|
||||
String currentId,
|
||||
FriendicaClient? client,) async {
|
||||
Future<List<EntryTreeItem>> processNewItems(
|
||||
List<TimelineEntry> items,
|
||||
String currentId,
|
||||
FriendicaClient? client,
|
||||
) async {
|
||||
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
||||
final allSeenItems = [...items];
|
||||
for (final item in items) {
|
||||
|
@ -229,9 +228,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
await StatusesClient(getIt<AccountsService>().currentProfile)
|
||||
.getPostOrComment(o.id, fullContext: true)
|
||||
.andThenSuccessAsync((items) async {
|
||||
final parentPostId = items
|
||||
.firstWhere((e) => e.parentId.isEmpty)
|
||||
.id;
|
||||
final parentPostId = items.firstWhere((e) => e.parentId.isEmpty).id;
|
||||
_parentPostIds[o.id] = parentPostId;
|
||||
allSeenItems.addAll(items);
|
||||
for (final item in items) {
|
||||
|
@ -259,7 +256,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
for (final item in seenItemsCopy) {
|
||||
if (item.parentId.isEmpty) {
|
||||
final postNode =
|
||||
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
||||
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
||||
postNodesToReturn.add(postNode);
|
||||
allSeenItems.remove(item);
|
||||
} else {
|
||||
|
@ -301,9 +298,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
.toList();
|
||||
|
||||
_logger.finest(
|
||||
'Completed processing new items ${client == null
|
||||
? 'sub level'
|
||||
: 'top level'}');
|
||||
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
|
||||
return updatedPosts;
|
||||
}
|
||||
|
||||
|
@ -312,11 +307,10 @@ class EntryManagerService extends ChangeNotifier {
|
|||
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
||||
final result = await client
|
||||
.getPostOrComment(id, fullContext: false)
|
||||
.andThenAsync((rootItems) async =>
|
||||
await client
|
||||
.getPostOrComment(id, fullContext: true)
|
||||
.andThenSuccessAsync(
|
||||
(contextItems) async => [...rootItems, ...contextItems]))
|
||||
.andThenAsync((rootItems) async => await client
|
||||
.getPostOrComment(id, fullContext: true)
|
||||
.andThenSuccessAsync(
|
||||
(contextItems) async => [...rootItems, ...contextItems]))
|
||||
.andThenSuccessAsync((items) async {
|
||||
await processNewItems(items, client.profile.username, null);
|
||||
});
|
||||
|
@ -325,7 +319,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_logger.finest('$id post updated');
|
||||
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
|
||||
}).mapError(
|
||||
(error) {
|
||||
(error) {
|
||||
_logger.finest('$id error updating: $error');
|
||||
return ExecError(
|
||||
type: ErrorType.localError,
|
||||
|
@ -339,7 +333,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_logger.finest('Resharing post: $id');
|
||||
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
||||
final result =
|
||||
await client.resharePost(id).andThenSuccessAsync((item) async {
|
||||
await client.resharePost(id).andThenSuccessAsync((item) async {
|
||||
await processNewItems([item], client.profile.username, null);
|
||||
});
|
||||
|
||||
|
@ -347,7 +341,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_logger.finest('$id post updated after reshare');
|
||||
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
|
||||
}).mapError(
|
||||
(error) {
|
||||
(error) {
|
||||
_logger.finest('$id error updating: $error');
|
||||
return ExecError(
|
||||
type: ErrorType.localError,
|
||||
|
@ -361,7 +355,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_logger.finest('Unresharing post: $id');
|
||||
final client = StatusesClient(getIt<AccountsService>().currentProfile);
|
||||
final result =
|
||||
await client.unResharePost(id).andThenSuccessAsync((item) async {
|
||||
await client.unResharePost(id).andThenSuccessAsync((item) async {
|
||||
await processNewItems([item], client.profile.username, null);
|
||||
});
|
||||
|
||||
|
@ -375,10 +369,9 @@ class EntryManagerService extends ChangeNotifier {
|
|||
return Result.ok(true);
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(String id,
|
||||
bool newStatus) async {
|
||||
final client =
|
||||
InteractionsClient(getIt<AccountsService>().currentProfile);
|
||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||
String id, bool newStatus) async {
|
||||
final client = InteractionsClient(getIt<AccountsService>().currentProfile);
|
||||
final result = await client.changeFavoriteStatus(id, newStatus);
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
|
@ -388,7 +381,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_entries[update.id] = update;
|
||||
final node = update.parentId.isEmpty
|
||||
? _postNodes[update.id]!
|
||||
: _postNodes[update.parentId]!.getChildById(update.id)!;
|
||||
: _postNodes[_parentPostIds[update.parentId]]!;
|
||||
|
||||
notifyListeners();
|
||||
return Result.ok(_nodeToTreeItem(node, client.profile.userId));
|
||||
|
@ -468,7 +461,7 @@ class _Node {
|
|||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
||||
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
|
|
@ -22,8 +22,13 @@ class NotificationsManager extends ChangeNotifier {
|
|||
idMapper: (nn) => nn.map((n) => n.id).toList(),
|
||||
onRequest: _clientGetNotificationsRequest,
|
||||
);
|
||||
var _firstLoad = true;
|
||||
|
||||
List<UserNotification> get notifications {
|
||||
if (_notifications.isEmpty && _firstLoad) {
|
||||
updateNotifications();
|
||||
_firstLoad = false;
|
||||
}
|
||||
final result = List<UserNotification>.from(_notifications.values);
|
||||
result.sort((n1, n2) {
|
||||
if (n1.dismissed == n2.dismissed) {
|
||||
|
@ -46,7 +51,7 @@ class NotificationsManager extends ChangeNotifier {
|
|||
}
|
||||
|
||||
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
||||
const initialPull = 25;
|
||||
const initialPull = 100;
|
||||
final nn = <UserNotification>[];
|
||||
if (_pm.pages.isEmpty) {
|
||||
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
|
||||
|
||||
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:
|
||||
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