diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b6efd6f06..53310da9b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,8 @@ + android:icon="@mipmap/launcher_icon" + android:requestLegacyExternalStorage="true"> LSRequiresIPhoneOS NSPhotoLibraryUsageDescription - 写真をアップロードするために、写真への権限を許可してください。 + 画像をアップロードするために、アルバムへの権限を許可してください。 + NSPhotoLibraryAddUsageDescription + 画像を保存するために、アルバムへの権限を許可してください。 UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/lib/view/common/common_drawer.dart b/lib/view/common/common_drawer.dart index 8efba1d9b..8c4ff50de 100644 --- a/lib/view/common/common_drawer.dart +++ b/lib/view/common/common_drawer.dart @@ -6,6 +6,7 @@ import 'package:miria/router/app_router.dart'; import 'package:miria/view/common/account_scope.dart'; import 'package:miria/view/common/avatar_icon.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:miria/view/common/misskey_notes/mfm_text.dart'; class CommonDrawer extends ConsumerWidget { final Account initialOpenAccount; @@ -27,7 +28,7 @@ class CommonDrawer extends ConsumerWidget { initiallyExpanded: account.userId == initialOpenAccount.userId && account.host == initialOpenAccount.host, - title: Text(account.i.name ?? account.i.username, + title: SimpleMfmText(account.i.name ?? account.i.username, style: Theme.of(context).textTheme.titleMedium), subtitle: Text( "@${account.userId}@${account.host}", diff --git a/lib/view/common/misskey_notes/image_dialog.dart b/lib/view/common/misskey_notes/image_dialog.dart index 63e37d3fe..08bf45a40 100644 --- a/lib/view/common/misskey_notes/image_dialog.dart +++ b/lib/view/common/misskey_notes/image_dialog.dart @@ -1,9 +1,14 @@ import 'dart:math'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:miria/providers.dart'; import 'package:miria/view/common/misskey_notes/network_image.dart'; -class ImageDialog extends StatefulWidget { +class ImageDialog extends ConsumerStatefulWidget { final List imageUrlList; final int initialPage; @@ -14,10 +19,10 @@ class ImageDialog extends StatefulWidget { }); @override - State createState() => ImageDialogState(); + ConsumerState createState() => ImageDialogState(); } -class ImageDialogState extends State { +class ImageDialogState extends ConsumerState { var scale = 1.0; late final pageController = PageController(initialPage: widget.initialPage); var verticalDragX = 0.0; @@ -123,7 +128,7 @@ class ImageDialogState extends State { ), )))), Positioned( - right: 10, + left: 10, top: 10, child: RawMaterialButton( onPressed: () { @@ -146,6 +151,40 @@ class ImageDialogState extends State { ?.color ?.withAlpha(200)))), ), + if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) + Positioned( + right: 10, + top: 10, + child: RawMaterialButton( + onPressed: () async { + final page = pageController.page?.toInt(); + if (page == null) return; + final response = await ref.read(dioProvider).get( + widget.imageUrlList[page], + options: + Options(responseType: ResponseType.bytes)); + await ImageGallerySaver.saveImage(response.data); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("画像保存したで"))); + }, + constraints: + const BoxConstraints(minWidth: 0, minHeight: 0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: EdgeInsets.zero, + fillColor: Theme.of(context) + .scaffoldBackgroundColor + .withAlpha(200), + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(5), + child: Icon(Icons.save, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withAlpha(200))))), ], ), )); diff --git a/lib/view/common/misskey_notes/mfm_text.dart b/lib/view/common/misskey_notes/mfm_text.dart index 12732e513..1a7d2961d 100644 --- a/lib/view/common/misskey_notes/mfm_text.dart +++ b/lib/view/common/misskey_notes/mfm_text.dart @@ -53,6 +53,7 @@ class MfmTextState extends ConsumerState { final account = AccountScope.of(context); // 他サーバーや外部サイトは別アプリで起動する + //TODO: nodeinfoから相手先サーバーがMisskeyの場合はそこで解決する if (uri.host != AccountScope.of(context).host) { if (await canLaunchUrl(uri)) { if (!await launchUrl(uri, @@ -69,6 +70,14 @@ class MfmTextState extends ConsumerState { uri.pathSegments.first == "channels") { context.pushRoute( ChannelDetailRoute(account: account, channelId: uri.pathSegments[1])); + } else if (uri.pathSegments.length == 2 && + uri.pathSegments.first == "notes") { + final note = await ref + .read(misskeyProvider(account)) + .notes + .show(NotesShowRequest(noteId: uri.pathSegments[1])); + if (!mounted) return; + context.pushRoute(NoteDetailRoute(account: account, note: note)); } else if (uri.pathSegments.length == 1 && uri.pathSegments.first.startsWith("@")) { await onMentionTap(uri.pathSegments.first); @@ -286,6 +295,15 @@ class UserInformation extends ConsumerStatefulWidget { } class UserInformationState extends ConsumerState { + String resolveIconUrl(Uri uri) { + final baseUrl = uri.toString(); + if (baseUrl.startsWith("/")) { + return "https://${widget.user.host ?? AccountScope.of(context).host}$baseUrl"; + } else { + return baseUrl; + } + } + @override Widget build(BuildContext context) { return SimpleMfmText( @@ -304,7 +322,7 @@ class UserInformationState extends ConsumerState { message: badge.name, child: NetworkImageView( type: ImageType.role, - url: badge.iconUrl.toString(), + url: resolveIconUrl(badge.iconUrl!), height: (DefaultTextStyle.of(context).style.fontSize ?? 22), ), ), diff --git a/lib/view/common/misskey_notes/note_modal_sheet.dart b/lib/view/common/misskey_notes/note_modal_sheet.dart index 49bce908e..f4bf97197 100644 --- a/lib/view/common/misskey_notes/note_modal_sheet.dart +++ b/lib/view/common/misskey_notes/note_modal_sheet.dart @@ -10,12 +10,14 @@ import 'package:miria/providers.dart'; import 'package:miria/router/app_router.dart'; import 'package:miria/view/common/misskey_notes/abuse_dialog.dart'; import 'package:miria/view/common/misskey_notes/clip_modal_sheet.dart'; +import 'package:miria/view/common/misskey_notes/open_another_account.dart'; import 'package:miria/view/common/not_implements_dialog.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; import 'package:misskey_dart/misskey_dart.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:share_plus/share_plus.dart'; @@ -97,6 +99,18 @@ class NoteModalSheet extends ConsumerWidget { Navigator.of(context).pop(); }), + if (targetNote.user.host != null) + ListTile( + title: const Text("ブラウザでリモート先を開く"), + onTap: () async { + final uri = targetNote.url ?? targetNote.uri; + if (uri == null) return; + launchUrl(uri, mode: LaunchMode.inAppWebView); + + Navigator.of(context).pop(); + }), + if (!targetNote.localOnly) + OpenAnotherAccount(note: targetNote, beforeOpenAccount: account), ListTile( title: const Text("ノートを共有"), onTap: () { diff --git a/lib/view/common/misskey_notes/open_another_account.dart b/lib/view/common/misskey_notes/open_another_account.dart new file mode 100644 index 000000000..151ee6f59 --- /dev/null +++ b/lib/view/common/misskey_notes/open_another_account.dart @@ -0,0 +1,124 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:miria/model/account.dart'; +import 'package:miria/providers.dart'; +import 'package:miria/router/app_router.dart'; +import 'package:miria/view/common/account_scope.dart'; +import 'package:miria/view/common/avatar_icon.dart'; +import 'package:miria/view/common/misskey_notes/mfm_text.dart'; +import 'package:misskey_dart/misskey_dart.dart'; + +class OpenAnotherAccount extends ConsumerStatefulWidget { + final Note note; + final Account beforeOpenAccount; + const OpenAnotherAccount({ + super.key, + required this.note, + required this.beforeOpenAccount, + }); + + @override + ConsumerState createState() => + OpenAnotherAccountState(); +} + +class OpenAnotherAccountState extends ConsumerState { + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text("別のアカウントで開く"), + onTap: () async { + final account = await showDialog( + context: context, + builder: (context2) => const AccountSelectDialog()); + + if (account == null) return; + + try { + // まず、自分のサーバーの直近のノートに該当のノートが含まれているか見る + final myHostUserData = await ref + .read(misskeyProvider(account)) + .users + .showByName(UsersShowByUserNameRequest( + userName: widget.note.user.username, + host: + widget.note.user.host ?? widget.beforeOpenAccount.host)); + + final myHostUserNotes = await ref + .read(misskeyProvider(account)) + .users + .notes(UsersNotesRequest( + userId: myHostUserData.id, + untilDate: widget.note.createdAt.millisecondsSinceEpoch + 1, + )); + + final foundMyHostNote = myHostUserNotes.firstWhereOrNull( + (e) => e.uri?.pathSegments.lastOrNull == widget.note.id); + if (foundMyHostNote != null) { + if (!mounted) return; + context.pushRoute( + NoteDetailRoute(note: foundMyHostNote, account: account)); + return; + } + throw Exception(); + } catch (e) { + // 最終手段として、連合で照会する + final result = await ref.read(misskeyProvider(account)).ap.show( + ApShowRequest( + uri: widget.note.uri ?? + Uri( + scheme: "https", + host: widget.beforeOpenAccount.host, + pathSegments: ["notes", widget.note.id]))); + // よくかんがえたら無駄 + final resultNote = await ref + .read(misskeyProvider(account)) + .notes + .show(NotesShowRequest(noteId: result.object["id"])); + if (!mounted) return; + context + .pushRoute(NoteDetailRoute(note: resultNote, account: account)); + } + }, + ); + } +} + +class AccountSelectDialog extends ConsumerWidget { + const AccountSelectDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final accounts = + ref.watch(accountRepository.select((value) => value.account)); + return AlertDialog( + title: const Text("開くアカウント選んでや"), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: ListView( + children: [ + for (final account in accounts) + AccountScope( + account: account, + child: ListTile( + leading: AvatarIcon.fromIResponse(account.i), + title: SimpleMfmText(account.i.name ?? account.i.username, + style: Theme.of(context).textTheme.titleMedium), + subtitle: Text( + "@${account.userId}@${account.host}", + style: Theme.of(context).textTheme.bodySmall, + ), + onTap: () { + Navigator.of(context).pop(account); + }, + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/view/note_detail_page/note_detail_page.dart b/lib/view/note_detail_page/note_detail_page.dart index adf29e6c4..642173548 100644 --- a/lib/view/note_detail_page/note_detail_page.dart +++ b/lib/view/note_detail_page/note_detail_page.dart @@ -6,6 +6,7 @@ import 'package:miria/extensions/date_time_extension.dart'; import 'package:miria/model/account.dart'; import 'package:miria/providers.dart'; import 'package:miria/view/common/account_scope.dart'; +import 'package:miria/view/common/common_drawer.dart'; import 'package:miria/view/common/misskey_notes/misskey_note.dart'; import 'package:miria/view/common/pushable_listview.dart'; import 'package:misskey_dart/misskey_dart.dart'; @@ -93,7 +94,7 @@ class NoteDetailPageState extends ConsumerState { Text( "投稿時間: ${actualShow!.createdAt.formatUntilMilliSeconds}"), const Padding(padding: EdgeInsets.only(top: 5)), - Divider(), + const Divider(), const Padding(padding: EdgeInsets.only(top: 5)), Padding( padding: const EdgeInsets.only(left: 20), diff --git a/lib/view/notification_page/notification_page.dart b/lib/view/notification_page/notification_page.dart index fa1d6dc39..a96df0cea 100644 --- a/lib/view/notification_page/notification_page.dart +++ b/lib/view/notification_page/notification_page.dart @@ -2,10 +2,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:miria/extensions/date_time_extension.dart'; import 'package:miria/model/account.dart'; import 'package:miria/model/misskey_emoji_data.dart'; import 'package:miria/providers.dart'; import 'package:miria/router/app_router.dart'; +import 'package:miria/view/notification_page/notification_page_data.dart'; import 'package:miria/view/common/account_scope.dart'; import 'package:miria/view/common/avatar_icon.dart'; import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; @@ -15,6 +17,7 @@ import 'package:miria/view/common/misskey_notes/misskey_note.dart' as misskey_note; import 'package:miria/view/common/pushable_listview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:miria/view/user_page/user_list_item.dart'; import 'package:misskey_dart/misskey_dart.dart'; @RoutePage() @@ -35,7 +38,7 @@ class NotificationPageState extends ConsumerState { appBar: AppBar(title: const Text("通知")), body: AccountScope( account: widget.account, - child: PushableListView( + child: PushableListView( initializeFuture: () async { final result = await misskey.i.notifications(const INotificationsRequest( @@ -49,7 +52,7 @@ class NotificationPageState extends ConsumerState { .read(mainStreamRepositoryProvider(widget.account)) .latestMarkAs(result.first.id); } - return result.toList(); + return result.toNotificationData(); }, nextFuture: (lastElement, _) async { final result = await misskey.i.notifications( @@ -57,7 +60,7 @@ class NotificationPageState extends ConsumerState { ref .read(notesProvider(widget.account)) .registerAll(result.map((e) => e.note).whereNotNull()); - return result.toList(); + return result.toNotificationData(); }, itemBuilder: (context, notification) => NotificationItem( notification: notification, @@ -69,161 +72,199 @@ class NotificationPageState extends ConsumerState { } class NotificationItem extends ConsumerWidget { - final INotificationsResponse notification; + final NotificationData notification; const NotificationItem({super.key, required this.notification}); @override Widget build(BuildContext context, WidgetRef ref) { - if (notification.type != NotificationType.reaction && - notification.type != NotificationType.renote && - notification.type != NotificationType.reply) { - if (kDebugMode) { - print(notification); - } - } + final notification = this.notification; - return Container( - padding: const EdgeInsets.all(10.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor))), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, + switch (notification) { + case RenoteReactionNotificationData(): + final hasRenote = notification.renoteUsers.isNotEmpty; + final hasReaction = notification.reactionUsers.isNotEmpty; + return Padding( + padding: + const EdgeInsets.only(left: 10, top: 10, bottom: 30, right: 10), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - if (notification.user != null) - GestureDetector( - onTap: () => context.pushRoute(UserRoute( - userId: notification.user!.id, - account: AccountScope.of(context))), - child: AvatarIcon( - user: notification.user!, - height: (Theme.of(context) - .textTheme - .displaySmall - ?.fontSize ?? - 22) * - MediaQuery.of(context).textScaleFactor, - ), + if (hasReaction && hasRenote) + Expanded( + child: SimpleMfmText( + "${notification.reactionUsers.first.$2?.name ?? notification.reactionUsers.first.$2?.username}さんたちがリアクションしはって、${notification.renoteUsers.first?.name ?? notification.renoteUsers.first?.username}さんたちがリノートしはったで", + emojis: Map.of( + notification.reactionUsers.first.$2?.emojis ?? {}) + ..addAll( + notification.renoteUsers.first?.emojis ?? {})), ), - if (notification.reaction != null) ...[ - const Padding(padding: EdgeInsets.only(top: 10)), - SizedBox( - width: 32, - height: 32, - child: CustomEmoji( - emojiData: MisskeyEmojiData.fromEmojiName( - emojiName: notification.reaction!, - repository: ref.read(emojiRepositoryProvider( - AccountScope.of(context))), - emojiInfo: notification.note?.reactionEmojis, - ), + if (hasReaction && !hasRenote) + Expanded( + child: SimpleMfmText( + "${notification.reactionUsers.first.$2?.name ?? notification.reactionUsers.first.$2?.username}さんたちがリアクションしはったで", + emojis: + notification.reactionUsers.first.$2?.emojis ?? {}, ), ), - ] + if (hasRenote && !hasReaction) + Expanded( + child: SimpleMfmText( + "${notification.renoteUsers.first?.name ?? notification.renoteUsers.first?.username}さんたちがリノートしはったで", + emojis: notification.renoteUsers.first?.emojis ?? {}), + ), + Text(notification.createdAt.differenceNow) ], ), - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (notification.type == NotificationType.reaction) ...[ - SimpleMfmText( - "${notification.user?.name ?? notification.user?.username} からリアクション", - emojis: notification.user?.emojis ?? {}, - ), - MediaQuery( - data: MediaQueryData( - textScaleFactor: - MediaQuery.of(context).textScaleFactor * 0.7), - child: misskey_note.MisskeyNote( - note: notification.note!, - isDisplayBorder: false, - )), - ], - if (notification.type == NotificationType.renote) ...[ - SimpleMfmText( - "${notification.user?.name ?? notification.user?.username} からRenote", - emojis: notification.user?.emojis ?? {}, - ), - MediaQuery( - data: MediaQueryData( - textScaleFactor: - MediaQuery.of(context).textScaleFactor * 0.7), - child: misskey_note.MisskeyNote( - note: notification.note!, - isDisplayBorder: false, + if (notification.note != null) + misskey_note.MisskeyNote( + note: notification.note!, + recursive: 2, + isDisplayBorder: false, + ), + Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + children: [ + if (hasRenote) + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).primaryColor)), + padding: const EdgeInsets.all(5), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("リノートしてくれはった人"), + Column( + children: [ + for (final user + in notification.renoteUsers.whereNotNull()) + UserListItem(user: user) + ], + ), + ], + ), ), - ) + if (hasReaction && hasRenote) + const Padding(padding: EdgeInsets.only(bottom: 10)), + if (hasReaction) + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).primaryColor)), + padding: const EdgeInsets.all(5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("リアクションしてくれはった人"), + for (final reaction in notification.reactionUsers + .mapIndexed( + (index, element) => (index, element))) ...[ + if (reaction.$2.$1 != null && + (reaction.$1 > 0 && + notification + .reactionUsers[ + reaction.$1 - 1] + .$1 != + reaction.$2.$1) || + reaction.$1 == 0) + CustomEmoji( + emojiData: MisskeyEmojiData.fromEmojiName( + emojiName: reaction.$2.$1!, + repository: ref.read( + emojiRepositoryProvider( + AccountScope.of(context))), + emojiInfo: + notification.note?.reactionEmojis, + ), + fontSizeRatio: 2, + ), + if (reaction.$2.$2 != null) + Padding( + padding: EdgeInsets.only(left: 20), + child: UserListItem(user: reaction.$2.$2!)), + ] + ], + ), + ) ], - if (notification.type == NotificationType.quote) ...[ - SimpleMfmText( - "${notification.user?.name ?? notification.user?.username} から引用Renote", + ), + ) + ], + ), + ); + + case MentionQuoteNotificationData(): + return Padding( + padding: + const EdgeInsets.only(left: 10, top: 10, bottom: 10, right: 10), + child: Column( + children: [ + if (notification.note != null) + misskey_note.MisskeyNote(note: notification.note!) + ], + ), + ); + case FollowNotificationData(): + return Padding( + padding: + const EdgeInsets.only(left: 10, top: 10, bottom: 10, right: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: SimpleMfmText( + "${notification.user?.name ?? notification.user?.username}から${notification.type.name}", emojis: notification.user?.emojis ?? {}, ), - MediaQuery( - data: MediaQueryData( - textScaleFactor: - MediaQuery.of(context).textScaleFactor * 0.7), - child: misskey_note.MisskeyNote( - note: notification.note!, - isDisplayBorder: false, - ), - ) - ], - if (notification.type == NotificationType.reply) ...[ - mfm_text.MfmText( - "${notification.user?.name ?? notification.user?.username} からリプライ", - ), - MediaQuery( - data: MediaQueryData( - textScaleFactor: - MediaQuery.of(context).textScaleFactor * 0.7), - child: misskey_note.MisskeyNote( - note: notification.note!, - isDisplayBorder: false, - )) - ], - if (notification.type == NotificationType.pollEnded) ...[ - const Text("投票が終わりました。"), - MediaQuery( - data: MediaQueryData( - textScaleFactor: - MediaQuery.of(context).textScaleFactor * 0.7), - child: misskey_note.MisskeyNote( - note: notification.note!, - isDisplayBorder: false, - )), - ], - if (notification.type == NotificationType.achievementEarned) - Text("実績を解除したで [${notification.achievement}]"), - if (notification.type == NotificationType.follow) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - const Padding(padding: EdgeInsets.only(top: 10)), - SimpleMfmText( - "${notification.user?.name ?? notification.user?.username} ", - style: Theme.of(context).textTheme.titleLarge, - ), - const Text("からフォローされました") - ], - ) + ), + Text(notification.createdAt.differenceNow), ], ), - ) - ], - )); + if (notification.user != null) + UserListItem(user: notification.user!), + ], + ), + ); + case SimpleNotificationData(): + return Padding( + padding: + const EdgeInsets.only(left: 10, top: 10, bottom: 10, right: 10), + child: Row( + children: [ + Expanded(child: Text(notification.text)), + Text(notification.createdAt.differenceNow), + ], + ), + ); + case PollNotification(): + return Padding( + padding: + const EdgeInsets.only(left: 10, top: 10, bottom: 10, right: 10), + child: Column( + children: [ + Row( + children: [ + const Expanded(child: Text("投票が終わったみたいや")), + Text(notification.createdAt.differenceNow), + ], + ), + misskey_note.MisskeyNote( + note: notification.note!, + isDisplayBorder: false, + ), + ], + ), + ); + } } } diff --git a/lib/view/notification_page/notification_page_data.dart b/lib/view/notification_page/notification_page_data.dart new file mode 100644 index 000000000..22db3e79c --- /dev/null +++ b/lib/view/notification_page/notification_page_data.dart @@ -0,0 +1,211 @@ +import 'package:misskey_dart/misskey_dart.dart'; + +sealed class NotificationData { + final String id; + final DateTime createdAt; + + NotificationData({required this.createdAt, required this.id}); +} + +class RenoteReactionNotificationData extends NotificationData { + final Note? note; + final List<(String?, User?)> reactionUsers; + final List renoteUsers; + + RenoteReactionNotificationData({ + required this.note, + required this.reactionUsers, + required this.renoteUsers, + required super.createdAt, + required super.id, + }); +} + +enum MentionQuoteNotificationDataType { + mention(name: "メンション"), + quote(name: "引用リノート"), + reply(name: ""); + + final String name; + const MentionQuoteNotificationDataType({required this.name}); +} + +class MentionQuoteNotificationData extends NotificationData { + final Note? note; + final User? user; + final MentionQuoteNotificationDataType type; + + MentionQuoteNotificationData({ + required super.createdAt, + required this.note, + required this.user, + required this.type, + required super.id, + }); +} + +enum FollowNotificationDataType { + follow("フォローされたで"), + followRequestAccepted("フォローしてもええでってなったで"), + receiveFollowRequest("フォローさせてほしそうにしてるで"); + + final String name; + const FollowNotificationDataType(this.name); +} + +class FollowNotificationData extends NotificationData { + final User? user; + final FollowNotificationDataType type; + FollowNotificationData({ + required this.user, + required super.createdAt, + required this.type, + required super.id, + }); +} + +class SimpleNotificationData extends NotificationData { + final String text; + + SimpleNotificationData({ + required this.text, + required super.createdAt, + required super.id, + }); +} + +class PollNotification extends NotificationData { + final Note? note; + + PollNotification({ + required this.note, + required super.createdAt, + required super.id, + }); +} + +extension INotificationsResponseExtension on Iterable { + List toNotificationData() { + final resultList = []; + + for (final element in this) { + switch (element.type) { + case NotificationType.reaction: + var isSummarize = false; + resultList + .whereType() + .where((e) => element.note?.id == e.note?.id) + .forEach((e) { + isSummarize = true; + e.reactionUsers.add((element.reaction!, element.user!)); + }); + + if (!isSummarize) { + resultList.add(RenoteReactionNotificationData( + note: element.note, + reactionUsers: [(element.reaction, element.user)], + renoteUsers: [], + createdAt: element.createdAt, + id: element.id)); + } + + break; + case NotificationType.renote: + var isSummarize = false; + resultList + .whereType() + .where((e) => element.note?.renote?.id == e.note?.id) + .forEach((e) { + isSummarize = true; + e.renoteUsers.add(element.user); + }); + + if (!isSummarize) { + resultList.add(RenoteReactionNotificationData( + note: element.note?.renote, + reactionUsers: [], + renoteUsers: [element.user], + createdAt: element.createdAt, + id: element.id)); + } + + break; + + case NotificationType.quote: + resultList.add(MentionQuoteNotificationData( + createdAt: element.createdAt, + note: element.note, + user: element.user, + type: MentionQuoteNotificationDataType.quote, + id: element.id)); + + break; + case NotificationType.mention: + resultList.add(MentionQuoteNotificationData( + createdAt: element.createdAt, + note: element.note, + user: element.user, + type: MentionQuoteNotificationDataType.mention, + id: element.id)); + + break; + case NotificationType.reply: + resultList.add(MentionQuoteNotificationData( + createdAt: element.createdAt, + note: element.note, + user: element.user, + type: MentionQuoteNotificationDataType.reply, + id: element.id)); + break; + + case NotificationType.follow: + resultList.add(FollowNotificationData( + user: element.user, + createdAt: element.createdAt, + type: FollowNotificationDataType.follow, + id: element.id)); + + break; + case NotificationType.followRequestAccepted: + resultList.add(FollowNotificationData( + user: element.user, + createdAt: element.createdAt, + type: FollowNotificationDataType.followRequestAccepted, + id: element.id)); + break; + case NotificationType.receiveFollowRequest: + resultList.add(FollowNotificationData( + user: element.user, + createdAt: element.createdAt, + type: FollowNotificationDataType.receiveFollowRequest, + id: element.id)); + break; + + case NotificationType.achievementEarned: + resultList.add(SimpleNotificationData( + text: "実績を解除しました。[${element.achievement}]", + createdAt: element.createdAt, + id: element.id)); + break; + + case NotificationType.pollVote: + resultList.add(PollNotification( + note: element.note, + createdAt: element.createdAt, + id: element.id)); + break; + case NotificationType.pollEnded: + resultList.add(PollNotification( + note: element.note, + createdAt: element.createdAt, + id: element.id)); + break; + + default: + break; + } + } + + return resultList; + } +} diff --git a/lib/view/time_line_page/time_line_page.dart b/lib/view/time_line_page/time_line_page.dart index ae76e86c2..37657ef02 100644 --- a/lib/view/time_line_page/time_line_page.dart +++ b/lib/view/time_line_page/time_line_page.dart @@ -123,8 +123,11 @@ class TimeLinePageState extends ConsumerState { @override Widget build(BuildContext context) { - final socketTimeline = ref.watch(widget.currentTabSetting.timelineProvider) - as SocketTimelineRepository?; + final socketTimelineBase = + ref.watch(widget.currentTabSetting.timelineProvider); + final socketTimeline = socketTimelineBase is SocketTimelineRepository + ? socketTimelineBase + : null; return AccountScope( account: widget.currentTabSetting.account, diff --git a/pubspec.lock b/pubspec.lock index 6add7230d..c32035896 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -608,6 +608,14 @@ packages: url: "https://github.com/shiosyakeyakini-info/flutter_image_editor_fix_ios_color_option.git" source: git version: "1.0.1" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" + url: "https://pub.dev" + source: hosted + version: "2.0.3" intl: dependency: "direct main" description: @@ -733,7 +741,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "2423681d5dd094f2da7f1bf18a22b3b6ab80f9cf" + resolved-ref: "1079937ea2e478928015425a498a2a56dc197b8e" url: "https://github.com/shiosyakeyakini-info/mfm_renderer.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 26ac387ff..b3e65918a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: miria description: Miria is Misskey Client for Mobile App. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.9+40 +version: 0.0.9+42 environment: sdk: '>=3.0.0 <4.0.0' @@ -53,6 +53,7 @@ dependencies: image_editor: ^1.3.0 json5: ^0.8.0 file: ^6.1.4 + image_gallery_saver: ^2.0.3 dependency_overrides: image_editor: