multi account notifs are live
This commit is contained in:
parent
6efa92c289
commit
5ef7f4e2e9
|
@ -1 +1,165 @@
|
|||
class NotificationModel {}
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localization/localization.dart';
|
||||
import 'package:loris/business_logic/account/account.dart';
|
||||
import 'package:loris/business_logic/timeline/timeline.dart';
|
||||
import '../../global.dart' as global;
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class NotificationModel implements Comparable {
|
||||
late String id;
|
||||
late String time;
|
||||
late NotificationType type;
|
||||
late AccountModel account;
|
||||
late PostModel? post;
|
||||
late String? accountid;
|
||||
NotificationModel.fromJson(Map<String, dynamic> json) {
|
||||
time = json["created_at"];
|
||||
id = json["id"];
|
||||
account = AccountModel.fromJson(json["account"]);
|
||||
type = NotificationType.values.firstWhere(
|
||||
(element) => element.name == "NotificationType.${json["type"]}");
|
||||
if (json["status"] != null) {
|
||||
post = PostModel.fromJson(json["status"]);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(other) {
|
||||
return time.compareTo(other.time);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationData {
|
||||
// stores latest post ids for each identity
|
||||
// value is null if there is no latest id
|
||||
final Map<String, String> latest;
|
||||
final List<NotificationModel> models;
|
||||
|
||||
NotificationData(this.latest, this.models);
|
||||
}
|
||||
|
||||
Future<NotificationData> loadOldNotifications(
|
||||
Map<String, String>? data,
|
||||
) async {
|
||||
List<NotificationModel> models = [];
|
||||
Map<String, String> map = data ?? {};
|
||||
|
||||
for (int i = 0; i < global.settings!.identities.length; i++) {
|
||||
final idkey = global.settings!.identities.keys.toList()[i];
|
||||
final id = global.settings!.identities[idkey]!;
|
||||
|
||||
Map<String, String> headers = id.getAuthHeaders();
|
||||
headers.addAll(global.defaultHeaders);
|
||||
|
||||
Map<String, String> query = {
|
||||
"limit": global.settings!.batchSize.toString()
|
||||
};
|
||||
if (map[idkey] != null) {
|
||||
query.addAll({"max_id": map[idkey]!});
|
||||
}
|
||||
|
||||
final uri = Uri(
|
||||
host: id.instanceUrl,
|
||||
path: "/api/v1/notifications",
|
||||
scheme: "https",
|
||||
queryParameters: query,
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
// get notifs
|
||||
if (response.statusCode == 200) {
|
||||
List<dynamic> json = jsonDecode(response.body);
|
||||
for (int n = 0; n < json.length; n++) {
|
||||
NotificationModel model = NotificationModel.fromJson(json[n]);
|
||||
model.accountid = idkey;
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// massage list
|
||||
models.sort();
|
||||
models = models.reversed.toList();
|
||||
models.removeRange(global.settings!.batchSize, models.length);
|
||||
|
||||
for (NotificationModel m in models) {
|
||||
if (map[m.accountid!] == null) {
|
||||
map.addAll({
|
||||
m.accountid!: m.id,
|
||||
});
|
||||
} else if (m.id.compareTo(map[m.accountid]!) < 0) {
|
||||
map.addAll({m.accountid!: m.id});
|
||||
}
|
||||
}
|
||||
|
||||
return NotificationData(map, models);
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
follow,
|
||||
followRequest,
|
||||
mention,
|
||||
reblog,
|
||||
favourite,
|
||||
poll,
|
||||
status,
|
||||
}
|
||||
|
||||
extension NotificationTypeExtension on NotificationType {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case NotificationType.followRequest:
|
||||
return "follow_request";
|
||||
default:
|
||||
return toString();
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case NotificationType.follow:
|
||||
return Icons.person_add;
|
||||
case NotificationType.followRequest:
|
||||
return Icons.person_add;
|
||||
case NotificationType.mention:
|
||||
return Icons.forum;
|
||||
case NotificationType.favourite:
|
||||
return Icons.favorite;
|
||||
case NotificationType.poll:
|
||||
return Icons.poll;
|
||||
case NotificationType.reblog:
|
||||
return Icons.repeat_on;
|
||||
case NotificationType.status:
|
||||
return Icons.forum;
|
||||
default:
|
||||
return Icons.question_mark;
|
||||
}
|
||||
}
|
||||
|
||||
String get actionName {
|
||||
switch (this) {
|
||||
case NotificationType.favourite:
|
||||
return "liked-your-post".i18n();
|
||||
case NotificationType.follow:
|
||||
return "followed-you".i18n();
|
||||
case NotificationType.followRequest:
|
||||
return "requested-to-folow-you".i18n();
|
||||
case NotificationType.mention:
|
||||
return "mentioned-you".i18n();
|
||||
case NotificationType.reblog:
|
||||
return "reblogged-your-post".i18n();
|
||||
case NotificationType.status:
|
||||
return "made-a-status".i18n();
|
||||
case NotificationType.poll:
|
||||
return "poll-has-ended".i18n();
|
||||
default:
|
||||
return "interacted-with-you".i18n();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,15 @@
|
|||
"content-width-percentage-label": "content width",
|
||||
"content-width-percentage-description": "determines what the regular width of content should be in percent (requires reloading the timeline)",
|
||||
"max-content-width-label": "max content width",
|
||||
"content-max-width-description": "determines what the maximum width of content should be, values below 375 get ignored because they would be too small (requires reloading the timeline)"
|
||||
"content-max-width-description": "determines what the maximum width of content should be, values below 375 get ignored because they would be too small (requires reloading the timeline)",
|
||||
"liked-your-post": "liked your post",
|
||||
"mentioned-you": "mentioned you",
|
||||
"reblogged-your-post": "reblogged your post",
|
||||
"followed-you": "followed you",
|
||||
"requested-to-folow-you": "requested to follow you",
|
||||
"made-a-status": "made a post",
|
||||
"poll-has-ended": "poll has ended",
|
||||
"interacted-with-you": "interacted with you"
|
||||
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localization/localization.dart';
|
||||
import 'package:loris/business_logic/notifications/notifs.dart';
|
||||
import 'package:loris/pages/notifications/single_notif.dart';
|
||||
import '../../business_logic/websocket.dart' as websocket;
|
||||
|
||||
final notifStream = StreamController<int>();
|
||||
final notifStream = StreamController<int>.broadcast();
|
||||
|
||||
class Notifications extends StatefulWidget {
|
||||
const Notifications({Key? key}) : super(key: key);
|
||||
|
@ -16,16 +18,39 @@ class Notifications extends StatefulWidget {
|
|||
|
||||
class _NotificationsState extends State<Notifications> {
|
||||
List<Widget> notifs = [];
|
||||
Map<String, String> maxIdData = {};
|
||||
final ScrollController _controller = ScrollController();
|
||||
|
||||
Future<void> loadMore() async {
|
||||
final data = await loadOldNotifications(maxIdData);
|
||||
final models = data.models;
|
||||
maxIdData = data.latest;
|
||||
List<Widget> widgets = [];
|
||||
for (int i = 0; i < models.length; i++) {
|
||||
widgets.add(
|
||||
SingleNotif(
|
||||
model: models[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
notifs.addAll(widgets);
|
||||
});
|
||||
cleanChildren();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
for (int i = 0; i < websocket.map.length; i++) {
|
||||
final keyI = websocket.map.keys.toList()[i];
|
||||
websocket.map[keyI]!["home"]!.stream.listen((event) async {
|
||||
Map<String, dynamic> json = jsonDecode(event);
|
||||
if (json["event"] == "notification") {
|
||||
SingleNotif notif = SingleNotif(
|
||||
content: jsonDecode(json["payload"]),
|
||||
model: NotificationModel.fromJson(jsonDecode(json["payload"])),
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
@ -36,16 +61,50 @@ class _NotificationsState extends State<Notifications> {
|
|||
}
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
loadMore();
|
||||
}
|
||||
|
||||
void cleanChildren() {
|
||||
setState(() {
|
||||
notifs.removeWhere((element) {
|
||||
return element.runtimeType != SingleNotif;
|
||||
});
|
||||
notifs.add(
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
loadMore();
|
||||
},
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
label: Text(
|
||||
"load-older-notifications".i18n(),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
itemBuilder: ((context, index) => notifs[index]),
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
itemCount: notifs.length);
|
||||
return Column(
|
||||
children: [
|
||||
Container(),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
controller: _controller,
|
||||
addSemanticIndexes: true,
|
||||
itemBuilder: ((context, index) => notifs[index]),
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
itemCount: notifs.length),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,77 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:loris/business_logic/notifications/notifs.dart';
|
||||
import 'package:loris/partials/post.dart';
|
||||
import '../../global.dart' as global;
|
||||
|
||||
class SingleNotif extends StatelessWidget {
|
||||
const SingleNotif({
|
||||
required this.content,
|
||||
required this.model,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final Map<String, dynamic> content;
|
||||
final NotificationModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(content["id"].toString());
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Align(
|
||||
child: Container(
|
||||
width:
|
||||
(MediaQuery.of(context).size.width * global.settings!.postWidth) -
|
||||
56,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: global.settings!.maxPostWidth,
|
||||
minWidth: 375,
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.all(color: Theme.of(context).colorScheme.secondary),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ProfilePic(url: model.account.avatar),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
model.account.displayName,
|
||||
style: Theme.of(context).textTheme.displaySmall,
|
||||
),
|
||||
SelectableText(
|
||||
"${model.account.acct} ${model.type.actionName}",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
model.type.icon,
|
||||
size: 64,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
(model.post != null)
|
||||
? (Post(
|
||||
model: model.post!,
|
||||
))
|
||||
: const SizedBox(
|
||||
width: 0,
|
||||
height: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:localization/localization.dart';
|
|||
import 'package:loris/partials/thread.dart';
|
||||
import '../../business_logic/timeline/timeline.dart' as tl;
|
||||
import '../../global.dart' as global;
|
||||
import 'package:loris/partials/loadingbox.dart';
|
||||
|
||||
class Timeline extends StatefulWidget {
|
||||
const Timeline({Key? key}) : super(key: key);
|
||||
|
@ -220,19 +221,3 @@ class TimelineState extends State<Timeline> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingBox extends StatelessWidget {
|
||||
const LoadingBox({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.fromSize(
|
||||
size: Size(double.infinity, MediaQuery.of(context).size.height),
|
||||
child: Center(
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size(128, 128),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoadingBox extends StatelessWidget {
|
||||
const LoadingBox({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.fromSize(
|
||||
size: Size(double.infinity, MediaQuery.of(context).size.height),
|
||||
child: Center(
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size(128, 128),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -30,6 +30,11 @@ class _MainScaffoldState extends State<MainScaffold> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
websocket.reloadWebsockets();
|
||||
|
@ -51,10 +56,7 @@ class _MainScaffoldState extends State<MainScaffold> {
|
|||
onPressed: () {},
|
||||
child: const Icon(Icons.person_add),
|
||||
),
|
||||
FloatingActionButton(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.clear_all),
|
||||
),
|
||||
null,
|
||||
null,
|
||||
];
|
||||
return Scaffold(
|
||||
|
|
|
@ -19,6 +19,7 @@ class _PostState extends State<Post> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DisplayName(account: widget.model.account),
|
||||
|
@ -54,11 +55,7 @@ class DisplayName extends StatelessWidget {
|
|||
}
|
||||
return Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ProfilePic(
|
||||
url: account.avatar,
|
||||
)),
|
||||
ProfilePic(url: account.avatar),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
|
@ -126,15 +123,18 @@ class ProfilePic extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
const double width = 64;
|
||||
if (url.isNotEmpty) {
|
||||
return Image.network(
|
||||
fit: BoxFit.cover,
|
||||
url,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.cruelty_free,
|
||||
size: width,
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
fit: BoxFit.cover,
|
||||
url,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.cruelty_free,
|
||||
size: width,
|
||||
),
|
||||
height: width,
|
||||
width: width,
|
||||
),
|
||||
height: width,
|
||||
width: width,
|
||||
);
|
||||
} else {
|
||||
return const Icon(
|
||||
|
|
Loading…
Reference in New Issue