Merge pull request 'Implement sharing media with Moxxy' (#98) from feat/sharing-intent into master
Reviewed-on: https://codeberg.org/moxxy/moxxyv2/pulls/98
This commit is contained in:
commit
a49bce8292
@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.moxxyv2">
|
package="org.moxxy.moxxyv2">
|
||||||
<!-- Flutter needs it to communicate with the running application
|
<!-- Flutter needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.moxxyv2">
|
package="org.moxxy.moxxyv2">
|
||||||
<application
|
<application
|
||||||
android:label="Moxxy"
|
android:label="Moxxy"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@ -7,7 +7,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
@ -18,16 +18,26 @@
|
|||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2"
|
android:value="2" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Allow receiving share intents for all kinds of things -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package com.example.moxxyv2
|
package org.moxxy.moxxyv2
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.moxxyv2">
|
package="org.moxxy.moxxyv2">
|
||||||
<!-- Flutter needs it to communicate with the running application
|
<!-- Flutter needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
|
@ -216,7 +216,7 @@ files:
|
|||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
jid: String
|
recipients: List<String>
|
||||||
body: String
|
body: String
|
||||||
chatState: String
|
chatState: String
|
||||||
quotedMessage:
|
quotedMessage:
|
||||||
@ -227,7 +227,7 @@ files:
|
|||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
jid: String
|
recipients: List<String>
|
||||||
paths: List<String>
|
paths: List<String>
|
||||||
- name: BlockJidCommand
|
- name: BlockJidCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
|
@ -19,6 +19,7 @@ import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
|||||||
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/profile_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/profile_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/sharedmedia_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/sharedmedia_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
import 'package:moxxyv2/ui/events.dart';
|
import 'package:moxxyv2/ui/events.dart';
|
||||||
@ -44,12 +45,14 @@ import 'package:moxxyv2/ui/pages/settings/licenses.dart';
|
|||||||
import 'package:moxxyv2/ui/pages/settings/network.dart';
|
import 'package:moxxyv2/ui/pages/settings/network.dart';
|
||||||
import 'package:moxxyv2/ui/pages/settings/privacy/privacy.dart';
|
import 'package:moxxyv2/ui/pages/settings/privacy/privacy.dart';
|
||||||
import 'package:moxxyv2/ui/pages/settings/settings.dart';
|
import 'package:moxxyv2/ui/pages/settings/settings.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/share_selection.dart';
|
||||||
import 'package:moxxyv2/ui/pages/sharedmedia.dart';
|
import 'package:moxxyv2/ui/pages/sharedmedia.dart';
|
||||||
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
|
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
|
||||||
import 'package:moxxyv2/ui/service/data.dart';
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
import 'package:moxxyv2/ui/service/progress.dart';
|
import 'package:moxxyv2/ui/service/progress.dart';
|
||||||
import 'package:moxxyv2/ui/service/thumbnail.dart';
|
import 'package:moxxyv2/ui/service/thumbnail.dart';
|
||||||
import 'package:page_transition/page_transition.dart';
|
import 'package:page_transition/page_transition.dart';
|
||||||
|
import 'package:share_handler/share_handler.dart';
|
||||||
|
|
||||||
void setupLogging() {
|
void setupLogging() {
|
||||||
Logger.root.level = Level.ALL;
|
Logger.root.level = Level.ALL;
|
||||||
@ -64,7 +67,7 @@ Future<void> setupUIServices() async {
|
|||||||
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
|
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
|
||||||
GetIt.I.registerSingleton<UIDataService>(UIDataService());
|
GetIt.I.registerSingleton<UIDataService>(UIDataService());
|
||||||
GetIt.I.registerSingleton<ThumbnailCacheService>(ThumbnailCacheService());
|
GetIt.I.registerSingleton<ThumbnailCacheService>(ThumbnailCacheService());
|
||||||
await GetIt.I.get<UIDataService>().init();}
|
}
|
||||||
|
|
||||||
void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
||||||
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navigationKey: navKey));
|
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navigationKey: navKey));
|
||||||
@ -79,6 +82,7 @@ void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
|||||||
GetIt.I.registerSingleton<CropBloc>(CropBloc());
|
GetIt.I.registerSingleton<CropBloc>(CropBloc());
|
||||||
GetIt.I.registerSingleton<SendFilesBloc>(SendFilesBloc());
|
GetIt.I.registerSingleton<SendFilesBloc>(SendFilesBloc());
|
||||||
GetIt.I.registerSingleton<CropBackgroundBloc>(CropBackgroundBloc());
|
GetIt.I.registerSingleton<CropBackgroundBloc>(CropBackgroundBloc());
|
||||||
|
GetIt.I.registerSingleton<ShareSelectionBloc>(ShareSelectionBloc());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(Unknown): Replace all Column(children: [ Padding(), Padding, ...]) with a
|
// TODO(Unknown): Replace all Column(children: [ Padding(), Padding, ...]) with a
|
||||||
@ -138,7 +142,10 @@ void main() async {
|
|||||||
),
|
),
|
||||||
BlocProvider<CropBackgroundBloc>(
|
BlocProvider<CropBackgroundBloc>(
|
||||||
create: (_) => GetIt.I.get<CropBackgroundBloc>(),
|
create: (_) => GetIt.I.get<CropBackgroundBloc>(),
|
||||||
)
|
),
|
||||||
|
BlocProvider<ShareSelectionBloc>(
|
||||||
|
create: (_) => GetIt.I.get<ShareSelectionBloc>(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: MyApp(navKey),
|
child: MyApp(navKey),
|
||||||
),
|
),
|
||||||
@ -164,6 +171,42 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
// Lift the UI block
|
// Lift the UI block
|
||||||
GetIt.I.get<Completer<void>>().complete();
|
GetIt.I.get<Completer<void>>().complete();
|
||||||
|
|
||||||
|
_setupSharingHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleSharedMedia(SharedMedia media) async {
|
||||||
|
final attachments = media.attachments ?? [];
|
||||||
|
GetIt.I.get<ShareSelectionBloc>().add(
|
||||||
|
ShareSelectionRequestedEvent(
|
||||||
|
attachments.map((a) => a!.path).toList(),
|
||||||
|
media.content,
|
||||||
|
media.content != null ? ShareSelectionType.text : ShareSelectionType.media,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupSharingHandler() async {
|
||||||
|
final handler = ShareHandlerPlatform.instance;
|
||||||
|
final media = await handler.getInitialSharedMedia();
|
||||||
|
|
||||||
|
// Shared while the app was closed
|
||||||
|
if (media != null) {
|
||||||
|
if (GetIt.I.get<UIDataService>().isLoggedIn) {
|
||||||
|
await _handleSharedMedia(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler.resetInitialSharedMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared while the app is stil running
|
||||||
|
handler.sharedMediaStream.listen((SharedMedia media) async {
|
||||||
|
if (GetIt.I.get<UIDataService>().isLoggedIn) {
|
||||||
|
await _handleSharedMedia(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler.resetInitialSharedMedia();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -263,6 +306,7 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
case cropRoute: return CropPage.route;
|
case cropRoute: return CropPage.route;
|
||||||
case sendFilesRoute: return SendFilesPage.route;
|
case sendFilesRoute: return SendFilesPage.route;
|
||||||
case backgroundCroppingRoute: return CropBackgroundPage.route;
|
case backgroundCroppingRoute: return CropBackgroundPage.route;
|
||||||
|
case shareSelectionRoute: return ShareSelectionPage.route;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -220,7 +220,7 @@ Future<void> performSetOpenConversation(SetOpenConversationCommand command, { dy
|
|||||||
Future<void> performSendMessage(SendMessageCommand command, { dynamic extra }) async {
|
Future<void> performSendMessage(SendMessageCommand command, { dynamic extra }) async {
|
||||||
await GetIt.I.get<XmppService>().sendMessage(
|
await GetIt.I.get<XmppService>().sendMessage(
|
||||||
body: command.body,
|
body: command.body,
|
||||||
jid: command.jid,
|
recipients: command.recipients,
|
||||||
chatState: command.chatState.isNotEmpty
|
chatState: command.chatState.isNotEmpty
|
||||||
? chatStateFromString(command.chatState)
|
? chatStateFromString(command.chatState)
|
||||||
: null,
|
: null,
|
||||||
@ -425,5 +425,5 @@ Future<void> performSignOut(SignOutCommand command, { dynamic extra }) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> performSendFiles(SendFilesCommand command, { dynamic extra }) async {
|
Future<void> performSendFiles(SendFilesCommand command, { dynamic extra }) async {
|
||||||
await GetIt.I.get<XmppService>().sendFiles(command.paths, command.jid);
|
await GetIt.I.get<XmppService>().sendFiles(command.paths, command.recipients);
|
||||||
}
|
}
|
||||||
|
@ -113,18 +113,26 @@ class HttpFileTransferService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _copyFile(String fromPath, String toPath, int msgId) async {
|
Future<void> _copyFile(FileUploadJob job) async {
|
||||||
await File(fromPath).copy(toPath);
|
for (final recipient in job.recipients) {
|
||||||
|
final newPath = await getDownloadPath(
|
||||||
|
pathlib.basename(job.path),
|
||||||
|
recipient,
|
||||||
|
job.mime,
|
||||||
|
);
|
||||||
|
|
||||||
|
await File(job.path).copy(newPath);
|
||||||
|
|
||||||
// Let the media scanner index the file
|
// Let the media scanner index the file
|
||||||
MoxplatformPlugin.media.scanFile(toPath);
|
MoxplatformPlugin.media.scanFile(newPath);
|
||||||
|
|
||||||
// Update the message
|
// Update the message
|
||||||
await GetIt.I.get<MessageService>().updateMessage(
|
await GetIt.I.get<MessageService>().updateMessage(
|
||||||
msgId,
|
job.messageMap[recipient]!.id,
|
||||||
mediaUrl: toPath,
|
mediaUrl: newPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Actually attempt to upload the file described by the job [job].
|
/// Actually attempt to upload the file described by the job [job].
|
||||||
Future<void> _performFileUpload(FileUploadJob job) async {
|
Future<void> _performFileUpload(FileUploadJob job) async {
|
||||||
@ -148,8 +156,6 @@ class HttpFileTransferService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final slot = slotResult.getValue();
|
final slot = slotResult.getValue();
|
||||||
final fileMime = lookupMimeType(job.path);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await dio.Dio().putUri<dynamic>(
|
final response = await dio.Dio().putUri<dynamic>(
|
||||||
Uri.parse(slot.putUrl),
|
Uri.parse(slot.putUrl),
|
||||||
@ -160,13 +166,17 @@ class HttpFileTransferService {
|
|||||||
),
|
),
|
||||||
data: data,
|
data: data,
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
|
// TODO(PapaTutuWawa): Make this smarter by also checking if one of those chats
|
||||||
|
// is open.
|
||||||
|
if (job.recipients.length == 1) {
|
||||||
final progress = count.toDouble() / total.toDouble();
|
final progress = count.toDouble() / total.toDouble();
|
||||||
sendEvent(
|
sendEvent(
|
||||||
ProgressEvent(
|
ProgressEvent(
|
||||||
id: job.message.id,
|
id: job.messageMap.values.first.id,
|
||||||
progress: progress == 1 ? 0.99 : progress,
|
progress: progress == 1 ? 0.99 : progress,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -176,8 +186,9 @@ class HttpFileTransferService {
|
|||||||
_log.severe('Upload failed');
|
_log.severe('Upload failed');
|
||||||
|
|
||||||
// Notify UI of upload failure
|
// Notify UI of upload failure
|
||||||
|
for (final recipient in job.recipients) {
|
||||||
final msg = await ms.updateMessage(
|
final msg = await ms.updateMessage(
|
||||||
job.message.id,
|
job.messageMap[recipient]!.id,
|
||||||
errorType: fileUploadFailedError,
|
errorType: fileUploadFailedError,
|
||||||
);
|
);
|
||||||
sendEvent(
|
sendEvent(
|
||||||
@ -185,11 +196,13 @@ class HttpFileTransferService {
|
|||||||
message: msg.copyWith(isUploading: false),
|
message: msg.copyWith(isUploading: false),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_log.fine('Upload was successful');
|
_log.fine('Upload was successful');
|
||||||
|
|
||||||
|
for (final recipient in job.recipients) {
|
||||||
// Notify UI of upload completion
|
// Notify UI of upload completion
|
||||||
var msg = job.message;
|
var msg = job.messageMap[recipient]!;
|
||||||
|
|
||||||
// Reset a stored error, if there was one
|
// Reset a stored error, if there was one
|
||||||
if (msg.errorType != null) {
|
if (msg.errorType != null) {
|
||||||
@ -207,14 +220,14 @@ class HttpFileTransferService {
|
|||||||
// Send the message to the recipient
|
// Send the message to the recipient
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
to: job.recipient,
|
to: recipient,
|
||||||
body: slot.getUrl,
|
body: slot.getUrl,
|
||||||
requestDeliveryReceipt: true,
|
requestDeliveryReceipt: true,
|
||||||
id: job.message.sid,
|
id: msg.sid,
|
||||||
originId: job.message.originId,
|
originId: msg.originId,
|
||||||
sfs: StatelessFileSharingData(
|
sfs: StatelessFileSharingData(
|
||||||
FileMetadataData(
|
FileMetadataData(
|
||||||
mediaType: fileMime,
|
mediaType: job.mime,
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
name: pathlib.basename(job.path),
|
name: pathlib.basename(job.path),
|
||||||
thumbnails: job.thumbnails,
|
thumbnails: job.thumbnails,
|
||||||
@ -223,14 +236,13 @@ class HttpFileTransferService {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_log.finest('Sent message with file upload for ${job.path}');
|
_log.finest('Sent message with file upload for ${job.path} to $recipient');
|
||||||
|
|
||||||
final isMultiMedia = fileMime != null ?
|
final isMultiMedia = job.mime?.startsWith('image/') == true || job.mime?.startsWith('video/') == true;
|
||||||
fileMime.startsWith('image/') || fileMime.startsWith('video/') :
|
|
||||||
false;
|
|
||||||
if (isMultiMedia) {
|
if (isMultiMedia) {
|
||||||
_log.finest('File appears to be either an image or a video. Copying it to the correct directory...');
|
_log.finest('File appears to be either an image or a video. Copying it to the correct directory...');
|
||||||
unawaited(_copyFile(job.path, job.copyToPath, msg.id));
|
unawaited(_copyFile(job));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on dio.DioError {
|
} on dio.DioError {
|
||||||
|
@ -6,25 +6,26 @@ import 'package:moxxyv2/xmpp/xeps/staging/extensible_file_thumbnails.dart';
|
|||||||
@immutable
|
@immutable
|
||||||
class FileUploadJob {
|
class FileUploadJob {
|
||||||
|
|
||||||
const FileUploadJob(this.recipient, this.path, this.copyToPath, this.message, this.thumbnails);
|
const FileUploadJob(this.recipients, this.path, this.mime, this.messageMap, this.thumbnails);
|
||||||
|
final List<String> recipients;
|
||||||
final String path;
|
final String path;
|
||||||
final String recipient;
|
final String? mime;
|
||||||
final Message message;
|
// Recipient -> Message
|
||||||
final String copyToPath;
|
final Map<String, Message> messageMap;
|
||||||
final List<Thumbnail> thumbnails;
|
final List<Thumbnail> thumbnails;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return other is FileUploadJob &&
|
return other is FileUploadJob &&
|
||||||
recipient == other.recipient &&
|
recipients == other.recipients &&
|
||||||
path == other.path &&
|
path == other.path &&
|
||||||
message == other.message &&
|
messageMap == other.messageMap &&
|
||||||
copyToPath == other.copyToPath &&
|
mime == other.mime &&
|
||||||
thumbnails == other.thumbnails;
|
thumbnails == other.thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => path.hashCode ^ recipient.hashCode ^ message.hashCode ^ copyToPath.hashCode ^ thumbnails.hashCode;
|
int get hashCode => path.hashCode ^ recipients.hashCode ^ messageMap.hashCode ^ mime.hashCode ^ thumbnails.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A job describing the upload of a file.
|
/// A job describing the upload of a file.
|
||||||
|
@ -189,10 +189,10 @@ class XmppService {
|
|||||||
/// Returns the JID of the chat that is currently opened. Null, if none is open.
|
/// Returns the JID of the chat that is currently opened. Null, if none is open.
|
||||||
String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
|
String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
|
||||||
|
|
||||||
/// Sends a message to [jid] with the body of [body].
|
/// Sends a message to JIDs in [recipients] with the body of [body].
|
||||||
Future<void> sendMessage({
|
Future<void> sendMessage({
|
||||||
required String body,
|
required String body,
|
||||||
required String jid,
|
required List<String> recipients,
|
||||||
Message? quotedMessage,
|
Message? quotedMessage,
|
||||||
String? commandId,
|
String? commandId,
|
||||||
ChatState? chatState,
|
ChatState? chatState,
|
||||||
@ -201,13 +201,15 @@ class XmppService {
|
|||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
final conn = GetIt.I.get<XmppConnection>();
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
for (final recipient in recipients) {
|
||||||
final sid = conn.generateId();
|
final sid = conn.generateId();
|
||||||
final originId = conn.generateId();
|
final originId = conn.generateId();
|
||||||
final message = await ms.addMessageFromData(
|
final message = await ms.addMessageFromData(
|
||||||
body,
|
body,
|
||||||
timestamp,
|
timestamp,
|
||||||
conn.getConnectionSettings().jid.toString(),
|
conn.getConnectionSettings().jid.toString(),
|
||||||
jid,
|
recipient,
|
||||||
false,
|
false,
|
||||||
sid,
|
sid,
|
||||||
false,
|
false,
|
||||||
@ -215,16 +217,15 @@ class XmppService {
|
|||||||
quoteId: quotedMessage?.originId ?? quotedMessage?.sid,
|
quoteId: quotedMessage?.originId ?? quotedMessage?.sid,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (commandId != null) {
|
// Using the same ID should be fine.
|
||||||
sendEvent(
|
sendEvent(
|
||||||
MessageAddedEvent(message: message),
|
MessageAddedEvent(message: message),
|
||||||
id: commandId,
|
id: commandId,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
to: jid,
|
to: recipient,
|
||||||
body: body,
|
body: body,
|
||||||
requestDeliveryReceipt: true,
|
requestDeliveryReceipt: true,
|
||||||
id: sid,
|
id: sid,
|
||||||
@ -236,7 +237,7 @@ class XmppService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final conversation = await cs.getConversationByJid(jid);
|
final conversation = await cs.getConversationByJid(recipient);
|
||||||
final newConversation = await cs.updateConversation(
|
final newConversation = await cs.updateConversation(
|
||||||
conversation!.id,
|
conversation!.id,
|
||||||
lastMessageBody: body,
|
lastMessageBody: body,
|
||||||
@ -247,6 +248,7 @@ class XmppService {
|
|||||||
ConversationUpdatedEvent(conversation: newConversation),
|
ConversationUpdatedEvent(conversation: newConversation),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String? _getMessageSrcUrl(MessageEvent event) {
|
String? _getMessageSrcUrl(MessageEvent event) {
|
||||||
if (event.sfs != null) {
|
if (event.sfs != null) {
|
||||||
@ -323,21 +325,13 @@ class XmppService {
|
|||||||
return GetIt.I.get<XmppConnection>().connectAwaitable(lastResource: lastResource);
|
return GetIt.I.get<XmppConnection>().connectAwaitable(lastResource: lastResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendFiles(List<String> paths, String recipient) async {
|
Future<void> sendFiles(List<String> paths, List<String> recipients) async {
|
||||||
// Create a new message
|
// Create a new message
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
|
||||||
// TODO(Unknown): This has a huge issue. The messages should get sent to the UI
|
// Path -> Recipient -> Message
|
||||||
// as soon as possible to indicate to the user that we are working on
|
final messages = <String, Map<String, Message>>{};
|
||||||
// them. But if the files are big, then copying them might take a little
|
|
||||||
// while. The solution might be to use the real path before copying as
|
|
||||||
// the messages initial mediaUrl attribute and once the file has been
|
|
||||||
// copied replace it with the new path. Meanwhile, the file can also be
|
|
||||||
// uploaded from its original location.
|
|
||||||
|
|
||||||
// Path -> Message
|
|
||||||
final messages = <String, Message>{};
|
|
||||||
// Path -> Thumbnails
|
// Path -> Thumbnails
|
||||||
final thumbnails = <String, List<Thumbnail>>{};
|
final thumbnails = <String, List<Thumbnail>>{};
|
||||||
|
|
||||||
@ -345,6 +339,8 @@ class XmppService {
|
|||||||
final conn = GetIt.I.get<XmppConnection>();
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
for (final path in paths) {
|
for (final path in paths) {
|
||||||
final pathMime = lookupMimeType(path);
|
final pathMime = lookupMimeType(path);
|
||||||
|
|
||||||
|
for (final recipient in recipients) {
|
||||||
final msg = await ms.addMessageFromData(
|
final msg = await ms.addMessageFromData(
|
||||||
'',
|
'',
|
||||||
DateTime.now().millisecondsSinceEpoch,
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
@ -357,12 +353,78 @@ class XmppService {
|
|||||||
mediaType: pathMime,
|
mediaType: pathMime,
|
||||||
originId: conn.generateId(),
|
originId: conn.generateId(),
|
||||||
);
|
);
|
||||||
messages[path] = msg;
|
if (messages.containsKey(path)) {
|
||||||
sendEvent(MessageAddedEvent(message: msg.copyWith(isUploading: true)));
|
messages[path]![recipient] = msg;
|
||||||
|
} else {
|
||||||
|
messages[path] = { recipient: msg };
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(MessageAddedEvent(message: msg.copyWith(isUploading: true)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the shared media entries
|
||||||
|
// Recipient -> [Shared Medium]
|
||||||
|
final sharedMediaMap = <String, List<DBSharedMedium>>{};
|
||||||
|
final rs = GetIt.I.get<RosterService>();
|
||||||
|
for (final recipient in recipients) {
|
||||||
|
for (final path in paths) {
|
||||||
|
final medium = await GetIt.I.get<DatabaseService>().addSharedMediumFromData(
|
||||||
|
path,
|
||||||
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
|
mime: lookupMimeType(path),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sharedMediaMap.containsKey(recipient)) {
|
||||||
|
sharedMediaMap[recipient]!.add(medium);
|
||||||
|
} else {
|
||||||
|
sharedMediaMap[recipient] = List<DBSharedMedium>.from([medium]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastFileMime = lookupMimeType(paths.last);
|
||||||
|
final conversation = await cs.getConversationByJid(recipient);
|
||||||
|
if (conversation != null) {
|
||||||
|
// Update conversation
|
||||||
|
final updatedConversation = await cs.updateConversation(
|
||||||
|
conversation.id,
|
||||||
|
lastMessageBody: mimeTypeToConversationBody(lastFileMime),
|
||||||
|
lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
sharedMedia: sharedMediaMap[recipient],
|
||||||
|
open: true,
|
||||||
|
);
|
||||||
|
sendEvent(ConversationUpdatedEvent(conversation: updatedConversation));
|
||||||
|
} else {
|
||||||
|
// Create conversation
|
||||||
|
final rosterItem = await rs.getRosterItemByJid(recipient);
|
||||||
|
final newConversation = await cs.addConversationFromData(
|
||||||
|
// TODO(Unknown): Should we use the JID parser?
|
||||||
|
rosterItem?.title ?? recipient.split('@').first,
|
||||||
|
mimeTypeToConversationBody(lastFileMime),
|
||||||
|
rosterItem?.avatarUrl ?? '',
|
||||||
|
recipient,
|
||||||
|
0,
|
||||||
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
|
sharedMediaMap[recipient]!,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notify the UI
|
||||||
|
sendEvent(ConversationAddedEvent(conversation: newConversation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requesting Upload slots and uploading
|
||||||
|
final hfts = GetIt.I.get<HttpFileTransferService>();
|
||||||
|
for (final path in paths) {
|
||||||
|
final pathMime = lookupMimeType(path);
|
||||||
|
|
||||||
|
for (final recipient in recipients) {
|
||||||
// TODO(PapaTutuWawa): Do this for videos
|
// TODO(PapaTutuWawa): Do this for videos
|
||||||
// TODO(PapaTutuWawa): Maybe do this in a separate isolate
|
// TODO(PapaTutuWawa): Maybe do this in a separate isolate
|
||||||
if ((pathMime ?? '').startsWith('image/')) {
|
if ((pathMime ?? '').startsWith('image/')) {
|
||||||
|
// Generate a thumbnail only when we have to
|
||||||
|
if (!thumbnails.containsKey(path)) {
|
||||||
final image = decodeImage((await File(path).readAsBytes()).toList());
|
final image = decodeImage((await File(path).readAsBytes()).toList());
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
thumbnails[path] = [BlurhashThumbnail(BlurHash.encode(image).hash)];
|
thumbnails[path] = [BlurhashThumbnail(BlurHash.encode(image).hash)];
|
||||||
@ -370,6 +432,7 @@ class XmppService {
|
|||||||
_log.warning('Failed to generate thumbnail for $path');
|
_log.warning('Failed to generate thumbnail for $path');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send an upload notification
|
// Send an upload notification
|
||||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
||||||
@ -386,38 +449,11 @@ class XmppService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the shared media entries
|
|
||||||
final sharedMedia = List<DBSharedMedium>.empty(growable: true);
|
|
||||||
for (final path in paths) {
|
|
||||||
sharedMedia.add(
|
|
||||||
await GetIt.I.get<DatabaseService>().addSharedMediumFromData(
|
|
||||||
path,
|
|
||||||
DateTime.now().millisecondsSinceEpoch,
|
|
||||||
mime: lookupMimeType(path),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update conversation
|
|
||||||
final lastFileMime = lookupMimeType(paths.last);
|
|
||||||
final conversationId = (await cs.getConversationByJid(recipient))!.id;
|
|
||||||
final updatedConversation = await cs.updateConversation(
|
|
||||||
conversationId,
|
|
||||||
lastMessageBody: mimeTypeToConversationBody(lastFileMime),
|
|
||||||
lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch,
|
|
||||||
sharedMedia: sharedMedia,
|
|
||||||
);
|
|
||||||
sendEvent(ConversationUpdatedEvent(conversation: updatedConversation));
|
|
||||||
|
|
||||||
// Requesting Upload slots and uploading
|
|
||||||
final hfts = GetIt.I.get<HttpFileTransferService>();
|
|
||||||
for (final path in paths) {
|
|
||||||
final pathMime = lookupMimeType(path);
|
|
||||||
await hfts.uploadFile(
|
await hfts.uploadFile(
|
||||||
FileUploadJob(
|
FileUploadJob(
|
||||||
recipient,
|
recipients,
|
||||||
path,
|
path,
|
||||||
await getDownloadPath(pathlib.basename(path), recipient, pathMime),
|
pathMime,
|
||||||
messages[path]!,
|
messages[path]!,
|
||||||
thumbnails[path] ?? [],
|
thumbnails[path] ?? [],
|
||||||
),
|
),
|
||||||
|
@ -176,7 +176,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
|||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
SendMessageCommand(
|
SendMessageCommand(
|
||||||
jid: state.conversation!.jid,
|
recipients: [state.conversation!.jid],
|
||||||
body: state.messageText,
|
body: state.messageText,
|
||||||
quotedMessage: state.quotedMessage,
|
quotedMessage: state.quotedMessage,
|
||||||
chatState: chatStateToString(ChatState.active),
|
chatState: chatStateToString(ChatState.active),
|
||||||
@ -301,13 +301,13 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
|||||||
|
|
||||||
Future<void> _onImagePickerRequested(ImagePickerRequestedEvent event, Emitter<ConversationState> emit) async {
|
Future<void> _onImagePickerRequested(ImagePickerRequestedEvent event, Emitter<ConversationState> emit) async {
|
||||||
GetIt.I.get<SendFilesBloc>().add(
|
GetIt.I.get<SendFilesBloc>().add(
|
||||||
SendFilesPageRequestedEvent(state.conversation!.jid, SendFilesType.image),
|
SendFilesPageRequestedEvent([state.conversation!.jid], SendFilesType.image),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onFilePickerRequested(FilePickerRequestedEvent event, Emitter<ConversationState> emit) async {
|
Future<void> _onFilePickerRequested(FilePickerRequestedEvent event, Emitter<ConversationState> emit) async {
|
||||||
GetIt.I.get<SendFilesBloc>().add(
|
GetIt.I.get<SendFilesBloc>().add(
|
||||||
SendFilesPageRequestedEvent(state.conversation!.jid, SendFilesType.generic),
|
SendFilesPageRequestedEvent([state.conversation!.jid], SendFilesType.generic),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||||
|
|
||||||
part 'conversations_bloc.freezed.dart';
|
part 'conversations_bloc.freezed.dart';
|
||||||
part 'conversations_event.dart';
|
part 'conversations_event.dart';
|
||||||
@ -30,16 +32,21 @@ class ConversationsBloc extends Bloc<ConversationsEvent, ConversationsState> {
|
|||||||
|
|
||||||
Future<void> _onConversationsAdded(ConversationsAddedEvent event, Emitter<ConversationsState> emit) async {
|
Future<void> _onConversationsAdded(ConversationsAddedEvent event, Emitter<ConversationsState> emit) async {
|
||||||
// TODO(Unknown): Should we guard against adding the same conversation multiple times?
|
// TODO(Unknown): Should we guard against adding the same conversation multiple times?
|
||||||
return emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
conversations: List.from(<Conversation>[ ...state.conversations, event.conversation ])
|
conversations: List.from(<Conversation>[ ...state.conversations, event.conversation ])
|
||||||
..sort(compareConversation),
|
..sort(compareConversation),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO(Unknown): Doing it from here feels absolutely not clean. Maybe change that.
|
||||||
|
GetIt.I.get<ShareSelectionBloc>().add(
|
||||||
|
ConversationsModified(state.conversations),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onConversationsUpdated(ConversationsUpdatedEvent event, Emitter<ConversationsState> emit) async {
|
Future<void> _onConversationsUpdated(ConversationsUpdatedEvent event, Emitter<ConversationsState> emit) async {
|
||||||
return emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
conversations: List.from(state.conversations.map((c) {
|
conversations: List.from(state.conversations.map((c) {
|
||||||
if (c.jid == event.conversation.jid) return event.conversation;
|
if (c.jid == event.conversation.jid) return event.conversation;
|
||||||
@ -48,6 +55,11 @@ class ConversationsBloc extends Bloc<ConversationsEvent, ConversationsState> {
|
|||||||
}).toList()..sort(compareConversation),),
|
}).toList()..sort(compareConversation),),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO(Unknown): Doing it from here feels absolutely not clean. Maybe change that.
|
||||||
|
GetIt.I.get<ShareSelectionBloc>().add(
|
||||||
|
ConversationsModified(state.conversations),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAvatarChanged(AvatarChangedEvent event, Emitter<ConversationsState> emit) async {
|
Future<void> _onAvatarChanged(AvatarChangedEvent event, Emitter<ConversationsState> emit) async {
|
||||||
|
@ -9,6 +9,7 @@ import 'package:moxxyv2/shared/models/conversation.dart';
|
|||||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
|
|
||||||
part 'login_bloc.freezed.dart';
|
part 'login_bloc.freezed.dart';
|
||||||
part 'login_event.dart';
|
part 'login_event.dart';
|
||||||
@ -72,6 +73,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result is LoginSuccessfulEvent) {
|
if (result is LoginSuccessfulEvent) {
|
||||||
|
GetIt.I.get<UIDataService>().isLoggedIn = true;
|
||||||
emit(state.copyWith(working: false));
|
emit(state.copyWith(working: false));
|
||||||
|
|
||||||
GetIt.I.get<ConversationsBloc>().add(
|
GetIt.I.get<ConversationsBloc>().add(
|
||||||
@ -91,6 +93,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (result is LoginFailureEvent) {
|
} else if (result is LoginFailureEvent) {
|
||||||
|
GetIt.I.get<UIDataService>().isLoggedIn = false;
|
||||||
return emit(
|
return emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
working: false,
|
working: false,
|
||||||
|
@ -41,4 +41,8 @@ class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
|
|||||||
Future<void> _onPoppedRoute(PoppedRouteEvent event, Emitter<NavigationState> emit) async {
|
Future<void> _onPoppedRoute(PoppedRouteEvent event, Emitter<NavigationState> emit) async {
|
||||||
navigationKey.currentState!.pop();
|
navigationKey.currentState!.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canPop() {
|
||||||
|
return navigationKey.currentState!.canPop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import 'package:moxxyv2/shared/models/conversation.dart';
|
|||||||
import 'package:moxxyv2/shared/models/roster.dart';
|
import 'package:moxxyv2/shared/models/roster.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart' as conversation;
|
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart' as conversation;
|
||||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||||
|
|
||||||
part 'newconversation_bloc.freezed.dart';
|
part 'newconversation_bloc.freezed.dart';
|
||||||
part 'newconversation_event.dart';
|
part 'newconversation_event.dart';
|
||||||
@ -99,6 +100,11 @@ class NewConversationBloc extends Bloc<NewConversationEvent, NewConversationStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(Unknown): Doing it from here feels absolutely not clean. Maybe change that.
|
||||||
|
GetIt.I.get<ShareSelectionBloc>().add(
|
||||||
|
RosterModifiedEvent(roster),
|
||||||
|
);
|
||||||
|
|
||||||
emit(state.copyWith(roster: roster));
|
emit(state.copyWith(roster: roster));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import 'package:moxxyv2/shared/preferences.dart';
|
|||||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
|
|
||||||
part 'preferences_event.dart';
|
part 'preferences_event.dart';
|
||||||
|
|
||||||
@ -43,6 +44,8 @@ class PreferencesBloc extends Bloc<PreferencesEvent, PreferencesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSignedOut(SignedOutEvent event, Emitter<PreferencesState> emit) async {
|
Future<void> _onSignedOut(SignedOutEvent event, Emitter<PreferencesState> emit) async {
|
||||||
|
GetIt.I.get<UIDataService>().isLoggedIn = false;
|
||||||
|
|
||||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
SignOutCommand(),
|
SignOutCommand(),
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:move_to_background/move_to_background.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
@ -32,26 +33,41 @@ class SendFilesBloc extends Bloc<SendFilesEvent, SendFilesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendFilesRequested(SendFilesPageRequestedEvent event, Emitter<SendFilesState> emit) async {
|
Future<void> _sendFilesRequested(SendFilesPageRequestedEvent event, Emitter<SendFilesState> emit) async {
|
||||||
final files = await _pickFiles(event.type);
|
List<String> files;
|
||||||
if (files == null) return;
|
if (event.paths != null) {
|
||||||
|
files = event.paths!;
|
||||||
|
} else {
|
||||||
|
final pickedFiles = await _pickFiles(event.type);
|
||||||
|
if (pickedFiles == null) return;
|
||||||
|
|
||||||
|
files = pickedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
files: files,
|
files: files,
|
||||||
index: 0,
|
index: 0,
|
||||||
conversationJid: event.jid,
|
recipients: event.recipients,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
NavigationEvent navEvent;
|
||||||
PushedNamedEvent(
|
if (event.popEntireStack) {
|
||||||
|
navEvent = PushedNamedAndRemoveUntilEvent(
|
||||||
|
const NavigationDestination(sendFilesRoute),
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
navEvent = PushedNamedEvent(
|
||||||
const NavigationDestination(
|
const NavigationDestination(
|
||||||
sendFilesRoute,
|
sendFilesRoute,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GetIt.I.get<NavigationBloc>().add(navEvent);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onIndexSet(IndexSetEvent event, Emitter<SendFilesState> emit) async {
|
Future<void> _onIndexSet(IndexSetEvent event, Emitter<SendFilesState> emit) async {
|
||||||
emit(state.copyWith(index: event.index));
|
emit(state.copyWith(index: event.index));
|
||||||
}
|
}
|
||||||
@ -71,13 +87,26 @@ class SendFilesBloc extends Bloc<SendFilesEvent, SendFilesState> {
|
|||||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
SendFilesCommand(
|
SendFilesCommand(
|
||||||
paths: state.files,
|
paths: state.files,
|
||||||
jid: state.conversationJid!,
|
recipients: state.recipients,
|
||||||
),
|
),
|
||||||
awaitable: false,
|
awaitable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return to the last page
|
// Return to the last page
|
||||||
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent());
|
final bloc = GetIt.I.get<NavigationBloc>();
|
||||||
|
final canPop = bloc.canPop();
|
||||||
|
NavigationEvent navEvent;
|
||||||
|
if (canPop) {
|
||||||
|
navEvent = PoppedRouteEvent();
|
||||||
|
} else {
|
||||||
|
navEvent = PushedNamedAndRemoveUntilEvent(
|
||||||
|
const NavigationDestination(conversationsRoute),
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bloc.add(navEvent);
|
||||||
|
if (!canPop) await MoveToBackground.moveTaskToBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onItemRemoved(ItemRemovedEvent event, Emitter<SendFilesState> emit) async {
|
Future<void> _onItemRemoved(ItemRemovedEvent event, Emitter<SendFilesState> emit) async {
|
||||||
|
@ -9,9 +9,11 @@ abstract class SendFilesEvent {}
|
|||||||
|
|
||||||
class SendFilesPageRequestedEvent extends SendFilesEvent {
|
class SendFilesPageRequestedEvent extends SendFilesEvent {
|
||||||
|
|
||||||
SendFilesPageRequestedEvent(this.jid, this.type);
|
SendFilesPageRequestedEvent(this.recipients, this.type, { this.paths, this.popEntireStack = false });
|
||||||
final String jid;
|
final List<String> recipients;
|
||||||
final SendFilesType type;
|
final SendFilesType type;
|
||||||
|
final List<String>? paths;
|
||||||
|
final bool popEntireStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
class IndexSetEvent extends SendFilesEvent {
|
class IndexSetEvent extends SendFilesEvent {
|
||||||
@ -22,7 +24,9 @@ class IndexSetEvent extends SendFilesEvent {
|
|||||||
|
|
||||||
class AddFilesRequestedEvent extends SendFilesEvent {}
|
class AddFilesRequestedEvent extends SendFilesEvent {}
|
||||||
|
|
||||||
class FileSendingRequestedEvent extends SendFilesEvent {}
|
class FileSendingRequestedEvent extends SendFilesEvent {
|
||||||
|
FileSendingRequestedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class ItemRemovedEvent extends SendFilesEvent {
|
class ItemRemovedEvent extends SendFilesEvent {
|
||||||
|
|
||||||
|
@ -8,6 +8,6 @@ class SendFilesState with _$SendFilesState {
|
|||||||
// The currently selected path
|
// The currently selected path
|
||||||
@Default(0) int index,
|
@Default(0) int index,
|
||||||
// The chat that is currently active
|
// The chat that is currently active
|
||||||
@Default(null) String? conversationJid,
|
@Default(<String>[]) List<String> recipients,
|
||||||
}) = _SendFilesState;
|
}) = _SendFilesState;
|
||||||
}
|
}
|
||||||
|
192
lib/ui/bloc/share_selection_bloc.dart
Normal file
192
lib/ui/bloc/share_selection_bloc.dart
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:move_to_background/move_to_background.dart';
|
||||||
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/roster.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
import 'package:moxxyv2/xmpp/xeps/xep_0085.dart';
|
||||||
|
|
||||||
|
part 'share_selection_bloc.freezed.dart';
|
||||||
|
part 'share_selection_event.dart';
|
||||||
|
part 'share_selection_state.dart';
|
||||||
|
|
||||||
|
/// The type of data we try to share
|
||||||
|
enum ShareSelectionType {
|
||||||
|
media,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a common ground between Conversations and RosterItems
|
||||||
|
class ShareListItem {
|
||||||
|
const ShareListItem(this.avatarPath, this.jid, this.title, this.isConversation);
|
||||||
|
final String avatarPath;
|
||||||
|
final String jid;
|
||||||
|
final String title;
|
||||||
|
final bool isConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShareSelectionBloc extends Bloc<ShareSelectionEvent, ShareSelectionState> {
|
||||||
|
ShareSelectionBloc() : super(ShareSelectionState()) {
|
||||||
|
on<ShareSelectionInitEvent>(_onShareSelectionInit);
|
||||||
|
on<ConversationsModified>(_onConversationsModified);
|
||||||
|
on<RosterModifiedEvent>(_onRosterModified);
|
||||||
|
on<ShareSelectionRequestedEvent>(_onRequested);
|
||||||
|
on<SelectionToggledEvent>(_onSelectionToggled);
|
||||||
|
on<SubmittedEvent>(_onSubmit);
|
||||||
|
on<ResetEvent>(_onReset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets user controllable data, i.e. paths, selections and text
|
||||||
|
void _resetState(Emitter<ShareSelectionState> emit) {
|
||||||
|
emit(state.copyWith(selection: [], paths: [], text: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of JIDs that are selected.
|
||||||
|
List<String> _getRecipients() {
|
||||||
|
return state.selection
|
||||||
|
.map((i) => state.items[i].jid)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateItems(List<Conversation> conversations, List<RosterItem> rosterItems, Emitter<ShareSelectionState> emit) {
|
||||||
|
// Use all conversations as a base
|
||||||
|
final items = List<ShareListItem>.from(
|
||||||
|
conversations.map((c) {
|
||||||
|
return ShareListItem(
|
||||||
|
c.avatarUrl,
|
||||||
|
c.jid,
|
||||||
|
c.title,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only add roster items with a JID which we don't already have in items.
|
||||||
|
for (final rosterItem in rosterItems) {
|
||||||
|
// We look for the index because this way we can update the roster items
|
||||||
|
final index = items.lastIndexWhere((ShareListItem e) => e.jid == rosterItem.jid);
|
||||||
|
if (index == -1) {
|
||||||
|
items.add(
|
||||||
|
ShareListItem(
|
||||||
|
rosterItem.avatarUrl,
|
||||||
|
rosterItem.jid,
|
||||||
|
rosterItem.title,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items[index] = ShareListItem(
|
||||||
|
rosterItem.avatarUrl,
|
||||||
|
rosterItem.jid,
|
||||||
|
rosterItem.title,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(items: items));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onShareSelectionInit(ShareSelectionInitEvent event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
_updateItems(event.conversations, event.rosterItems, emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRequested(ShareSelectionRequestedEvent event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
emit(state.copyWith(paths: event.paths, text: event.text, type: event.type));
|
||||||
|
|
||||||
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
|
PushedNamedAndRemoveUntilEvent(
|
||||||
|
const NavigationDestination(shareSelectionRoute),
|
||||||
|
(_) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onConversationsModified(ConversationsModified event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
_updateItems(
|
||||||
|
event.conversations,
|
||||||
|
GetIt.I.get<NewConversationBloc>().state.roster,
|
||||||
|
emit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRosterModified(RosterModifiedEvent event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
_updateItems(
|
||||||
|
GetIt.I.get<ConversationsBloc>().state.conversations,
|
||||||
|
event.rosterItems,
|
||||||
|
emit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSubmit(SubmittedEvent event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
if (state.type == ShareSelectionType.text) {
|
||||||
|
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
|
SendMessageCommand(
|
||||||
|
recipients: _getRecipients(),
|
||||||
|
body: state.text!,
|
||||||
|
chatState: chatStateToString(ChatState.gone),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate to the conversations page...
|
||||||
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
|
PushedNamedAndRemoveUntilEvent(
|
||||||
|
const NavigationDestination(conversationsRoute),
|
||||||
|
(_) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// ...reset the state...
|
||||||
|
_resetState(emit);
|
||||||
|
// ...and put the app back into the background
|
||||||
|
await MoveToBackground.moveTaskToBack();
|
||||||
|
} else {
|
||||||
|
GetIt.I.get<SendFilesBloc>().add(
|
||||||
|
SendFilesPageRequestedEvent(
|
||||||
|
state.selection
|
||||||
|
.map((i) => state.items[i].jid)
|
||||||
|
.toList(),
|
||||||
|
// TODO(PapaTutuWawa): Fix
|
||||||
|
SendFilesType.image,
|
||||||
|
paths: state.paths,
|
||||||
|
popEntireStack: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_resetState(emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSelectionToggled(SelectionToggledEvent event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
if (state.selection.contains(event.index)) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selection: List.from(
|
||||||
|
state.selection
|
||||||
|
.where((s) => s != event.index)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selection: List.from(
|
||||||
|
[...state.selection, event.index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onReset(ResetEvent event, Emitter<ShareSelectionState> emit) async {
|
||||||
|
_resetState(emit);
|
||||||
|
}
|
||||||
|
}
|
43
lib/ui/bloc/share_selection_event.dart
Normal file
43
lib/ui/bloc/share_selection_event.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
part of 'share_selection_bloc.dart';
|
||||||
|
|
||||||
|
abstract class ShareSelectionEvent {}
|
||||||
|
|
||||||
|
/// Triggered when we receive the initial data, i.e. open conversations and the roster
|
||||||
|
class ShareSelectionInitEvent extends ShareSelectionEvent {
|
||||||
|
ShareSelectionInitEvent(this.conversations, this.rosterItems);
|
||||||
|
final List<Conversation> conversations;
|
||||||
|
final List<RosterItem> rosterItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when the share page has been requested. [paths] refers to the paths that
|
||||||
|
/// we want to share with the JID or the JIDs.
|
||||||
|
class ShareSelectionRequestedEvent extends ShareSelectionEvent {
|
||||||
|
ShareSelectionRequestedEvent(this.paths, this.text, this.type);
|
||||||
|
final List<String> paths;
|
||||||
|
final String? text;
|
||||||
|
final ShareSelectionType type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when we want to toggle the selection of a list item
|
||||||
|
class SelectionToggledEvent extends ShareSelectionEvent {
|
||||||
|
SelectionToggledEvent(this.index);
|
||||||
|
final int index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when a conversation gets added or removed
|
||||||
|
class ConversationsModified extends ShareSelectionEvent {
|
||||||
|
ConversationsModified(this.conversations);
|
||||||
|
final List<Conversation> conversations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when the roster gets modified
|
||||||
|
class RosterModifiedEvent extends ShareSelectionEvent {
|
||||||
|
RosterModifiedEvent(this.rosterItems);
|
||||||
|
final List<RosterItem> rosterItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when the user confirms their selection.
|
||||||
|
class SubmittedEvent extends ShareSelectionEvent {}
|
||||||
|
|
||||||
|
/// Triggered when we should reset the paths and the selection
|
||||||
|
class ResetEvent extends ShareSelectionEvent {}
|
17
lib/ui/bloc/share_selection_state.dart
Normal file
17
lib/ui/bloc/share_selection_state.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
part of 'share_selection_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ShareSelectionState with _$ShareSelectionState {
|
||||||
|
factory ShareSelectionState({
|
||||||
|
// A deduplicated combination of the conversation and roster list
|
||||||
|
@Default(<ShareListItem>[]) List<ShareListItem> items,
|
||||||
|
// List of paths that we want to share
|
||||||
|
@Default(<String>[]) List<String> paths,
|
||||||
|
// The text we want to share
|
||||||
|
@Default(null) String? text,
|
||||||
|
// List of selected items in items
|
||||||
|
@Default(<int>[]) List<int> selection,
|
||||||
|
// The type of data we try to share
|
||||||
|
@Default(ShareSelectionType.media) ShareSelectionType type,
|
||||||
|
}) = _ShareSelectionState;
|
||||||
|
}
|
@ -11,10 +11,10 @@ const EdgeInsetsGeometry textfieldPaddingConversation = EdgeInsets.all(10);
|
|||||||
const int primaryColorHexRGBO = 0xffcf4aff;
|
const int primaryColorHexRGBO = 0xffcf4aff;
|
||||||
const Color primaryColor = Color(primaryColorHexRGBO);
|
const Color primaryColor = Color(primaryColorHexRGBO);
|
||||||
|
|
||||||
const Color bubbleColorSent = Color(0xffac70ca);
|
const Color bubbleColorSent = Color(0xffa139f0);
|
||||||
const Color bubbleColorSentQuoted = Color(0xff964db3);
|
const Color bubbleColorSentQuoted = bubbleColorSent;
|
||||||
const Color bubbleColorReceived = Color(0xff222222);
|
const Color bubbleColorReceived = Color(0xff222222);
|
||||||
const Color bubbleColorReceivedQuoted = Color(0xff2c3e50);
|
const Color bubbleColorReceivedQuoted = bubbleColorReceived;
|
||||||
|
|
||||||
const double paddingVeryLarge = 64;
|
const double paddingVeryLarge = 64;
|
||||||
|
|
||||||
@ -51,3 +51,4 @@ const String networkRoute = '$settingsRoute/network';
|
|||||||
const String appearanceRoute = '$settingsRoute/appearance';
|
const String appearanceRoute = '$settingsRoute/appearance';
|
||||||
const String backgroundCroppingRoute = '$settingsRoute/appearance/background';
|
const String backgroundCroppingRoute = '$settingsRoute/appearance/background';
|
||||||
const String blocklistRoute = '/blocklist';
|
const String blocklistRoute = '/blocklist';
|
||||||
|
const String shareSelectionRoute = '/share_selection';
|
||||||
|
@ -38,6 +38,7 @@ PopupMenuItem<dynamic> popupItemWithIcon(dynamic value, String text, IconData ic
|
|||||||
|
|
||||||
/// A custom version of the Topbar NameAndAvatar style to integrate with
|
/// A custom version of the Topbar NameAndAvatar style to integrate with
|
||||||
/// bloc.
|
/// bloc.
|
||||||
|
// TODO(Unknown): If the display name is too long, then it will cause an overflow.
|
||||||
class ConversationTopbarWidget extends StatelessWidget {
|
class ConversationTopbarWidget extends StatelessWidget {
|
||||||
const ConversationTopbarWidget({ Key? key }) : super(key: key);
|
const ConversationTopbarWidget({ Key? key }) : super(key: key);
|
||||||
|
|
||||||
|
103
lib/ui/pages/share_selection.dart
Normal file
103
lib/ui/pages/share_selection.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:move_to_background/move_to_background.dart';
|
||||||
|
import 'package:moxxyv2/shared/constants.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart' as navigation;
|
||||||
|
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/conversation.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/topbar.dart';
|
||||||
|
|
||||||
|
class ShareSelectionPage extends StatelessWidget {
|
||||||
|
const ShareSelectionPage({ Key? key }) : super(key: key);
|
||||||
|
|
||||||
|
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
|
||||||
|
builder: (_) => const ShareSelectionPage(),
|
||||||
|
settings: const RouteSettings(
|
||||||
|
name: shareSelectionRoute,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
bool _buildWhen(ShareSelectionState prev, ShareSelectionState next) {
|
||||||
|
// Prevent rebuilding when items changes. This prevents us from having to deal with
|
||||||
|
// a roster update coming in while we are selecting JIDs to share to.
|
||||||
|
// TODO(Unknown): But does it work?
|
||||||
|
return prev.selection != next.selection ||
|
||||||
|
prev.paths != next.paths ||
|
||||||
|
prev.text != next.text ||
|
||||||
|
prev.type != next.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final maxTextWidth = MediaQuery.of(context).size.width * 0.6;
|
||||||
|
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
GetIt.I.get<ShareSelectionBloc>().add(ResetEvent());
|
||||||
|
|
||||||
|
// Navigate to the conversations page...
|
||||||
|
GetIt.I.get<navigation.NavigationBloc>().add(
|
||||||
|
navigation.PushedNamedAndRemoveUntilEvent(
|
||||||
|
const navigation.NavigationDestination(conversationsRoute),
|
||||||
|
(_) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// ...and put the app back into the background
|
||||||
|
await MoveToBackground.moveTaskToBack();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: BlocBuilder<ShareSelectionBloc, ShareSelectionState>(
|
||||||
|
buildWhen: _buildWhen,
|
||||||
|
builder: (context, state) => Scaffold(
|
||||||
|
appBar: BorderlessTopbar.simple('Share with...'),
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: state.items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = state.items[index];
|
||||||
|
final isSelected = state.selection.contains(index);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
context.read<ShareSelectionBloc>().add(
|
||||||
|
SelectionToggledEvent(index),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ConversationsListRow(
|
||||||
|
item.avatarPath,
|
||||||
|
item.title,
|
||||||
|
item.jid,
|
||||||
|
0,
|
||||||
|
maxTextWidth,
|
||||||
|
timestampNever,
|
||||||
|
false,
|
||||||
|
extra: Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (_) {
|
||||||
|
context.read<ShareSelectionBloc>().add(
|
||||||
|
SelectionToggledEvent(index),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
floatingActionButton: state.selection.isNotEmpty ?
|
||||||
|
FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ShareSelectionBloc>().add(SubmittedEvent());
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.send,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
) :
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,10 @@ import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
|||||||
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
|
import 'package:share_handler/share_handler.dart';
|
||||||
|
|
||||||
/// Handler for when we received a [PreStartDoneEvent].
|
/// Handler for when we received a [PreStartDoneEvent].
|
||||||
Future<void> preStartDone(PreStartDoneEvent result, { dynamic extra }) async {
|
Future<void> preStartDone(PreStartDoneEvent result, { dynamic extra }) async {
|
||||||
@ -21,6 +24,7 @@ Future<void> preStartDone(PreStartDoneEvent result, { dynamic extra }) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.state == preStartLoggedInState) {
|
if (result.state == preStartLoggedInState) {
|
||||||
|
GetIt.I.get<UIDataService>().isLoggedIn = true;
|
||||||
GetIt.I.get<ConversationsBloc>().add(
|
GetIt.I.get<ConversationsBloc>().add(
|
||||||
ConversationsInitEvent(
|
ConversationsInitEvent(
|
||||||
result.displayName!,
|
result.displayName!,
|
||||||
@ -37,13 +41,26 @@ Future<void> preStartDone(PreStartDoneEvent result, { dynamic extra }) async {
|
|||||||
GetIt.I.get<ConversationBloc>().add(OwnJidReceivedEvent(result.jid!));
|
GetIt.I.get<ConversationBloc>().add(OwnJidReceivedEvent(result.jid!));
|
||||||
|
|
||||||
GetIt.I.get<Logger>().finest('Navigating to conversations');
|
GetIt.I.get<Logger>().finest('Navigating to conversations');
|
||||||
|
|
||||||
|
// Only go to the Conversations page when we did not start due to a sharing intent
|
||||||
|
final handler = ShareHandlerPlatform.instance;
|
||||||
|
if (await handler.getInitialSharedMedia() == null) {
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PushedNamedAndRemoveUntilEvent(
|
PushedNamedAndRemoveUntilEvent(
|
||||||
const NavigationDestination(conversationsRoute),
|
const NavigationDestination(conversationsRoute),
|
||||||
(_) => false,
|
(_) => false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GetIt.I.get<ShareSelectionBloc>().add(
|
||||||
|
ShareSelectionInitEvent(
|
||||||
|
result.conversations!,
|
||||||
|
result.roster!,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (result.state == preStartNotLoggedInState) {
|
} else if (result.state == preStartNotLoggedInState) {
|
||||||
|
GetIt.I.get<UIDataService>().isLoggedIn = false;
|
||||||
GetIt.I.get<Logger>().finest('Navigating to intro');
|
GetIt.I.get<Logger>().finest('Navigating to intro');
|
||||||
GetIt.I.get<NavigationBloc>().add(
|
GetIt.I.get<NavigationBloc>().add(
|
||||||
PushedNamedAndRemoveUntilEvent(
|
PushedNamedAndRemoveUntilEvent(
|
||||||
|
@ -1,36 +1,6 @@
|
|||||||
import 'package:external_path/external_path.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:moxxyv2/shared/models/message.dart';
|
|
||||||
import 'package:path/path.dart' as pathlib;
|
|
||||||
|
|
||||||
class UIDataService {
|
class UIDataService {
|
||||||
|
|
||||||
UIDataService();
|
UIDataService() : isLoggedIn = false;
|
||||||
late String _thumbnailBase;
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
final base = await ExternalPath.getExternalStoragePublicDirectory(ExternalPath.DIRECTORY_PICTURES);
|
|
||||||
_thumbnailBase = pathlib.join(base, 'Moxxy', '.thumbnail');
|
|
||||||
}
|
|
||||||
|
|
||||||
// The base path for thumbnails
|
|
||||||
String get thumbnailBase => _thumbnailBase;
|
|
||||||
|
|
||||||
/// Returns the path of a possible thumbnail for the video. Does not imply that the file
|
|
||||||
/// exists.
|
|
||||||
String getThumbnailPath(Message message) => getThumbnailPathFull(
|
|
||||||
message.conversationJid,
|
|
||||||
pathlib.basename(message.mediaUrl!),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Returns the path of a possible thumbnail for the video. Does not imply that the file
|
|
||||||
/// exists.
|
|
||||||
String getThumbnailPathFull(String conversationJid, String filename) => pathlib.join(
|
|
||||||
_thumbnailBase,
|
|
||||||
conversationJid,
|
|
||||||
filename,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
bool isLoggedIn;
|
||||||
}
|
}
|
||||||
|
@ -38,15 +38,14 @@ class TextChatWidget extends StatelessWidget {
|
|||||||
child: ParsedText(
|
child: ParsedText(
|
||||||
text: message.body,
|
text: message.body,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: const Color(0xf9ebffff),
|
||||||
fontSize: fontsize,
|
fontSize: fontsize,
|
||||||
),
|
),
|
||||||
parse: [
|
parse: [
|
||||||
MatchText(
|
MatchText(
|
||||||
type: ParsedType.URL,
|
type: ParsedType.URL,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
// TODO(Unknown): Work on the color
|
decoration: TextDecoration.underline,
|
||||||
color: Colors.blue,
|
|
||||||
),
|
),
|
||||||
onTap: (url) async {
|
onTap: (url) async {
|
||||||
await launchUrl(
|
await launchUrl(
|
||||||
|
@ -19,6 +19,7 @@ class ConversationsListRow extends StatefulWidget {
|
|||||||
this.lastChangeTimestamp,
|
this.lastChangeTimestamp,
|
||||||
this.update, {
|
this.update, {
|
||||||
this.typingIndicator = false,
|
this.typingIndicator = false,
|
||||||
|
this.extra,
|
||||||
Key? key,
|
Key? key,
|
||||||
}
|
}
|
||||||
) : super(key: key);
|
) : super(key: key);
|
||||||
@ -30,6 +31,7 @@ class ConversationsListRow extends StatefulWidget {
|
|||||||
final int lastChangeTimestamp;
|
final int lastChangeTimestamp;
|
||||||
final bool update; // Should a timer run to update the timestamp
|
final bool update; // Should a timer run to update the timestamp
|
||||||
final bool typingIndicator;
|
final bool typingIndicator;
|
||||||
|
final Widget? extra;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConversationsListRowState createState() => ConversationsListRowState();
|
ConversationsListRowState createState() => ConversationsListRowState();
|
||||||
@ -98,8 +100,12 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
|||||||
final badgeText = widget.unreadCount > 99 ? '99+' : widget.unreadCount.toString();
|
final badgeText = widget.unreadCount > 99 ? '99+' : widget.unreadCount.toString();
|
||||||
// TODO(Unknown): Maybe turn this into an attribute of the widget to prevent calling this
|
// TODO(Unknown): Maybe turn this into an attribute of the widget to prevent calling this
|
||||||
// for every conversation
|
// for every conversation
|
||||||
final width = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final width = screenWidth - 24 - 70;
|
||||||
|
final textWidth = screenWidth * 0.6;
|
||||||
|
|
||||||
|
final showTimestamp = widget.lastChangeTimestamp != timestampNever;
|
||||||
|
final showBadge = widget.unreadCount > 0;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -111,58 +117,60 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8),
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: LimitedBox(
|
||||||
|
maxWidth: width,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Row(
|
||||||
width: width - 70.0 - 16.0 - 8.0,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Text(
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: widget.maxTextWidth,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
widget.name,
|
widget.name,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 17),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 17),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: widget.lastChangeTimestamp != timestampNever,
|
visible: showTimestamp,
|
||||||
|
child: const Spacer(),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: showTimestamp,
|
||||||
child: Text(_timestampString),
|
child: Text(_timestampString),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
SizedBox(
|
Row(
|
||||||
width: width - 70.0 - 16.0 - 8.0,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
// TODO(Unknown): Change color and font size
|
LimitedBox(
|
||||||
Container(
|
maxWidth: textWidth,
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: widget.maxTextWidth,
|
|
||||||
),
|
|
||||||
// TODO(Unknown): Colors
|
|
||||||
child: _buildLastMessageBody(),
|
child: _buildLastMessageBody(),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
Visibility(
|
||||||
|
visible: showBadge,
|
||||||
|
child: const Spacer(),
|
||||||
|
),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: widget.unreadCount > 0,
|
visible: widget.unreadCount > 0,
|
||||||
child: Badge(
|
child: Badge(
|
||||||
badgeContent: Text(badgeText),
|
badgeContent: Text(badgeText),
|
||||||
badgeColor: bubbleColorSent,
|
badgeColor: bubbleColorSent,
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: widget.extra != null,
|
||||||
|
child: const Spacer(),
|
||||||
|
),
|
||||||
|
...widget.extra != null ? [widget.extra!] : [],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
37
pubspec.lock
37
pubspec.lock
@ -664,6 +664,15 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
move_to_background:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: e5cc2eefd1667e8ef22f21f41b0ef012b060be6c
|
||||||
|
resolved-ref: e5cc2eefd1667e8ef22f21f41b0ef012b060be6c
|
||||||
|
url: "https://github.com/ViliusP/move_to_background.git"
|
||||||
|
source: git
|
||||||
|
version: "2.0.0"
|
||||||
moxdns:
|
moxdns:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -965,6 +974,34 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
share_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_handler
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.16"
|
||||||
|
share_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_handler_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.6"
|
||||||
|
share_handler_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_handler_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.9"
|
||||||
|
share_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_handler_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.5"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -46,6 +46,10 @@ dependencies:
|
|||||||
json_annotation: 4.6.0
|
json_annotation: 4.6.0
|
||||||
logging: 1.0.2
|
logging: 1.0.2
|
||||||
mime: 1.0.2
|
mime: 1.0.2
|
||||||
|
move_to_background:
|
||||||
|
git:
|
||||||
|
url: https://github.com/ViliusP/move_to_background.git
|
||||||
|
ref: e5cc2eefd1667e8ef22f21f41b0ef012b060be6c
|
||||||
moxdns:
|
moxdns:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
@ -69,6 +73,7 @@ dependencies:
|
|||||||
random_string: 2.3.1
|
random_string: 2.3.1
|
||||||
saslprep: 1.0.2
|
saslprep: 1.0.2
|
||||||
settings_ui: 2.0.2
|
settings_ui: 2.0.2
|
||||||
|
share_handler: 0.0.16
|
||||||
#scrollable_positioned_list: 0.2.3
|
#scrollable_positioned_list: 0.2.3
|
||||||
stack_blur: 0.2.2
|
stack_blur: 0.2.2
|
||||||
swipeable_tile:
|
swipeable_tile:
|
||||||
|
Loading…
Reference in New Issue
Block a user