Add private API attachment sending

This commit is contained in:
Tanay Neotia
2023-02-04 17:03:50 -05:00
parent 15175fc359
commit 5cc5d4fad3
11 changed files with 142 additions and 94 deletions

View File

@ -651,6 +651,7 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
null,
null,
null,
false,
);
} else {
if (!(createCompleter?.isCompleted ?? true)) return;

View File

@ -36,7 +36,7 @@ void sendEffectAction(
String? chatGuid,
Future<void> Function({String? effect}) sendMessage,
) {
if (!ss.settings.enablePrivateAPI.value || (text.isEmpty && subjectText.isEmpty)) return;
if (!ss.settings.enablePrivateAPI.value) return;
String typeSelected = "bubble";
final bubbleEffects = ["slam", "loud", "gentle", "invisible ink"];
final screenEffects = ["echo", "spotlight", "balloons", "confetti", "love", "lasers", "fireworks", "celebration"];
@ -283,90 +283,91 @@ void sendEffectAction(
),
),
const Spacer(),
Theme(
data: context.theme.copyWith(
// in case some components still use legacy theming
primaryColor: context.theme.colorScheme.bubble(context, true),
colorScheme: context.theme.colorScheme.copyWith(
primary: context.theme.colorScheme.bubble(context, true),
onPrimary: context.theme.colorScheme.onBubble(context, true),
surface: ss.settings.monetTheming.value == Monet.full
? null
: (context.theme.extensions[BubbleColors] as BubbleColors?)?.receivedBubbleColor,
onSurface: ss.settings.monetTheming.value == Monet.full
? null
: (context.theme.extensions[BubbleColors] as BubbleColors?)?.onReceivedBubbleColor,
if (text.isNotEmpty || subjectText.isNotEmpty)
Theme(
data: context.theme.copyWith(
// in case some components still use legacy theming
primaryColor: context.theme.colorScheme.bubble(context, true),
colorScheme: context.theme.colorScheme.copyWith(
primary: context.theme.colorScheme.bubble(context, true),
onPrimary: context.theme.colorScheme.onBubble(context, true),
surface: ss.settings.monetTheming.value == Monet.full
? null
: (context.theme.extensions[BubbleColors] as BubbleColors?)?.receivedBubbleColor,
onSurface: ss.settings.monetTheming.value == Monet.full
? null
: (context.theme.extensions[BubbleColors] as BubbleColors?)?.onReceivedBubbleColor,
),
),
),
child: Builder(
builder: (context) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
key: key,
padding: const EdgeInsets.only(right: 5.0),
child: BubbleEffects(
globalKey: key,
part: 0,
message: message,
showTail: true,
child: ClipPath(
clipper: TailClipper(
isFromMe: true,
showTail: true,
connectLower: false,
connectUpper: false,
),
child: Container(
key: key,
constraints: BoxConstraints(
maxWidth: ns.width(context) * MessageWidgetController.maxBubbleSizeFactor - 40,
minHeight: 40,
child: Builder(
builder: (context) {
return Align(
alignment: Alignment.centerRight,
child: Padding(
key: key,
padding: const EdgeInsets.only(right: 5.0),
child: BubbleEffects(
globalKey: key,
part: 0,
message: message,
showTail: true,
child: ClipPath(
clipper: TailClipper(
isFromMe: true,
showTail: true,
connectLower: false,
connectUpper: false,
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15).add(const EdgeInsets.only(right: 10)),
color: context.theme.colorScheme.primary,
child: CustomAnimationBuilder<Movie>(
control: animController,
tween: MovieTween()
..scene(begin: Duration.zero, duration: const Duration(milliseconds: 1), curve: Curves.easeInOut)
.tween("size", 1.0.tweenTo(1.0))
..scene(begin: const Duration(milliseconds: 1), duration: const Duration(milliseconds: 500), curve: Curves.easeInOut)
.tween("size", 0.0.tweenTo(0.5))
..scene(begin: const Duration(milliseconds: 1000), duration: const Duration(milliseconds: 800), curve: Curves.easeInOut)
.tween("size", 0.5.tweenTo(1.0)),
duration: const Duration(milliseconds: 1800),
animationStatusListener: (status) {
if (status == AnimationStatus.completed) {
setState(() {
animController = Control.stop;
});
}
},
builder: (context, anim, child) {
final value1 = anim.get("size");
return Transform.scale(
scale: value1,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: buildMessageSpans(
context,
MessagePart(part: 0, text: message.text),
message,
child: Container(
key: key,
constraints: BoxConstraints(
maxWidth: ns.width(context) * MessageWidgetController.maxBubbleSizeFactor - 40,
minHeight: 40,
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15).add(const EdgeInsets.only(right: 10)),
color: context.theme.colorScheme.primary,
child: CustomAnimationBuilder<Movie>(
control: animController,
tween: MovieTween()
..scene(begin: Duration.zero, duration: const Duration(milliseconds: 1), curve: Curves.easeInOut)
.tween("size", 1.0.tweenTo(1.0))
..scene(begin: const Duration(milliseconds: 1), duration: const Duration(milliseconds: 500), curve: Curves.easeInOut)
.tween("size", 0.0.tweenTo(0.5))
..scene(begin: const Duration(milliseconds: 1000), duration: const Duration(milliseconds: 800), curve: Curves.easeInOut)
.tween("size", 0.5.tweenTo(1.0)),
duration: const Duration(milliseconds: 1800),
animationStatusListener: (status) {
if (status == AnimationStatus.completed) {
setState(() {
animController = Control.stop;
});
}
},
builder: (context, anim, child) {
final value1 = anim.get("size");
return Transform.scale(
scale: value1,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: buildMessageSpans(
context,
MessagePart(part: 0, text: message.text),
message,
),
),
),
),
);
},
);
},
),
),
),
),
),
),
);
}
);
}
),
),
),
const Spacer(),
TextButton(
child: Text(

View File

@ -35,7 +35,7 @@ class _SendAnimationState extends CustomState<SendAnimation, Tuple6<List<Platfor
});
}
Future<void> send(Tuple6<List<PlatformFile>, String, String, String?, int?, String?> tuple) async {
Future<void> send(Tuple6<List<PlatformFile>, String, String, String?, int?, String?> tuple, bool isAudioMessage) async {
// do not add anything above this line, the attachments must be extracted first
final attachments = List<PlatformFile>.from(tuple.item1);
final text = tuple.item2;
@ -67,6 +67,9 @@ class _SendAnimationState extends CustomState<SendAnimation, Tuple6<List<Platfor
],
isFromMe: true,
handleId: 0,
threadOriginatorGuid: i == 0 ? replyGuid : null,
threadOriginatorPart: i == 0 ? "${part ?? 0}:0:0" : null,
expressiveSendStyleId: effectId,
);
message.generateTempGuid();
message.attachments.first!.guid = message.guid;
@ -74,6 +77,7 @@ class _SendAnimationState extends CustomState<SendAnimation, Tuple6<List<Platfor
type: QueueType.sendAttachment,
chat: controller.chat,
message: message,
customArgs: {"audio": isAudioMessage}
));
}
@ -81,8 +85,8 @@ class _SendAnimationState extends CustomState<SendAnimation, Tuple6<List<Platfor
final _message = Message(
text: text.isEmpty && subject.isNotEmpty ? subject : text,
subject: text.isEmpty && subject.isNotEmpty ? null : subject,
threadOriginatorGuid: replyGuid,
threadOriginatorPart: "${part ?? 0}:0:0",
threadOriginatorGuid: attachments.isEmpty ? replyGuid : null,
threadOriginatorPart: attachments.isEmpty ? "${part ?? 0}:0:0" : null,
expressiveSendStyleId: effectId,
dateCreated: DateTime.now(),
hasAttachments: false,

View File

@ -263,13 +263,6 @@ class ConversationTextFieldState extends CustomState<ConversationTextField, void
showSnackbar("Error", "Something went wrong!");
}
} else {
if (controller.textController.text.isEmpty && controller.subjectTextController.text.isEmpty) {
if (controller.replyToMessage != null) {
return showSnackbar("Error", "Replies must be sent with a text message!");
} else if (effect != null) {
return showSnackbar("Error", "Effects must be sent with a text message!");
}
}
await controller.send(
controller.pickedAttachments,
controller.textController.text,
@ -277,6 +270,7 @@ class ConversationTextFieldState extends CustomState<ConversationTextField, void
controller.replyToMessage?.item1.threadOriginatorGuid ?? controller.replyToMessage?.item1.guid,
controller.replyToMessage?.item2,
effect,
false,
);
}
controller.pickedAttachments.clear();

View File

@ -135,7 +135,7 @@ class _TextFieldSuffixState extends OptimizedState<TextFieldSuffix> {
onPressed: () async {
await widget.controller!.send(
[file],
"", "", null, null, null,
"", "", null, null, null, true,
);
deleteAudioRecording(file.path!);
Get.back();

View File

@ -351,6 +351,32 @@ class _PrivateAPIPanelState extends CustomState<PrivateAPIPanel, void, PrivateAP
],
),
),
AnimatedSizeAndFade.showHide(
show: controller.serverVersionCode.value >= 208,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: tileColor,
child: Padding(
padding: const EdgeInsets.only(left: 15.0),
child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant),
),
),
SettingsSwitch(
onChanged: (bool val) {
ss.settings.privateAPIAttachmentSend.value = val;
saveSettings();
},
initialVal: ss.settings.privateAPIAttachmentSend.value,
title: "Private API Attachment Send",
subtitle: "Send attachments using the Private API",
backgroundColor: tileColor,
isThreeLine: true,
),
],
),
),
],
)
]

View File

@ -78,6 +78,7 @@ class Settings {
final RxBool privateManualMarkAsRead = false.obs;
final RxBool privateSubjectLine = false.obs;
final RxBool privateAPISend = false.obs;
final RxBool privateAPIAttachmentSend = false.obs;
// Redacted Mode Settings
final RxBool redactedMode = false.obs;
@ -234,6 +235,7 @@ class Settings {
'receiveSoundPath': receiveSoundPath.value,
'syncContactsAutomatically': syncContactsAutomatically.value,
'privateAPISend': privateAPISend.value,
'privateAPIAttachmentSend': privateAPIAttachmentSend.value,
'highlightSelectedChat': highlightSelectedChat.value,
'enablePrivateAPI': enablePrivateAPI.value,
'privateSendTypingIndicators': privateSendTypingIndicators.value,
@ -340,6 +342,7 @@ class Settings {
ss.settings.receiveSoundPath.value = map['receiveSoundPath'];
ss.settings.syncContactsAutomatically.value = map['syncContactsAutomatically'];
ss.settings.privateAPISend.value = map['privateAPISend'] ?? false;
ss.settings.privateAPIAttachmentSend.value = map['privateAPIAttachmentSend'] ?? false;
ss.settings.enablePrivateAPI.value = map['enablePrivateAPI'] ?? false;
ss.settings.privateSendTypingIndicators.value = map['privateSendTypingIndicators'] ?? false;
ss.settings.privateMarkChatAsRead.value = map['privateMarkChatAsRead'] ?? false;
@ -450,6 +453,7 @@ class Settings {
s.receiveSoundPath.value = map['receiveSoundPath'];
s.syncContactsAutomatically.value = map['syncContactsAutomatically'] ?? false;
s.privateAPISend.value = map['privateAPISend'] ?? false;
s.privateAPIAttachmentSend.value = map['privateAPIAttachmentSend'] ?? false;
s.enablePrivateAPI.value = map['enablePrivateAPI'] ?? false;
s.privateSendTypingIndicators.value = map['privateSendTypingIndicators'] ?? false;
s.privateMarkChatAsRead.value = map['privateMarkChatAsRead'] ?? false;

View File

@ -142,14 +142,27 @@ class ActionHandler extends GetxService {
await c.addMessage(m);
}
Future<void> sendAttachment(Chat c, Message m) async {
Future<void> sendAttachment(Chat c, Message m, bool isAudioMessage) async {
if (m.attachments.isEmpty || m.attachments.firstOrNull?.bytes == null) return;
final attachment = m.attachments.first!;
final progress = attachmentProgress.firstWhere((e) => e.item1 == attachment.guid);
final completer = Completer<void>();
latestCancelToken = CancelToken();
http.sendAttachment(c.guid, attachment.guid!, PlatformFile(name: attachment.transferName!, bytes: attachment.bytes, path: attachment.path, size: attachment.totalBytes ?? 0),
http.sendAttachment(
c.guid,
attachment.guid!,
PlatformFile(name: attachment.transferName!, bytes: attachment.bytes, path: attachment.path, size: attachment.totalBytes ?? 0),
onSendProgress: (count, total) => progress.item2.value = count / attachment.bytes!.length,
method: (ss.settings.enablePrivateAPI.value
&& ss.settings.privateAPIAttachmentSend.value)
|| (m.subject?.isNotEmpty ?? false)
|| m.threadOriginatorGuid != null
|| m.expressiveSendStyleId != null
? "private-api" : "apple-script",
selectedMessageGuid: m.threadOriginatorGuid,
effectId: m.expressiveSendStyleId,
partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""),
isAudioMessage: isAudioMessage,
cancelToken: latestCancelToken,
).then((response) async {
latestCancelToken = null;

View File

@ -35,7 +35,7 @@ class OutgoingQueue extends Queue {
await ah.sendMessage(item.chat, item.message, item.selected, item.reaction);
break;
case QueueType.sendAttachment:
await ah.sendAttachment(item.chat, item.message);
await ah.sendAttachment(item.chat, item.message, item.customArgs?['audio'] ?? false);
break;
default:
Logger.info("Unhandled queue event: ${describeEnum(item.type)}");

View File

@ -518,7 +518,7 @@ class HttpService extends GetxService {
/// Send an attachment. [chatGuid] specifies the chat, [tempGuid] specifies a
/// temporary guid to avoid duplicate messages being sent, [file] is the
/// body of the message.
Future<Response> sendAttachment(String chatGuid, String tempGuid, PlatformFile file, {void Function(int, int)? onSendProgress, CancelToken? cancelToken}) async {
Future<Response> sendAttachment(String chatGuid, String tempGuid, PlatformFile file, {void Function(int, int)? onSendProgress, String? method, String? effectId, String? subject, String? selectedMessageGuid, int? partIndex, bool? isAudioMessage, CancelToken? cancelToken}) async {
return runApiGuarded(() async {
final fileName = file.path!.split('/').last;
final formData = FormData.fromMap({
@ -526,6 +526,11 @@ class HttpService extends GetxService {
"chatGuid": chatGuid,
"tempGuid": tempGuid,
"name": fileName,
"method": method,
"effectId": effectId,
"subject": subject,
"selectedMessageGuid": selectedMessageGuid,
"partIndex": partIndex,
});
final response = await dio.post(
"$origin/message/attachment",

View File

@ -75,7 +75,7 @@ class ConversationViewController extends StatefulController with SingleGetTicker
bool keyboardOpen = false;
double _keyboardOffset = 0;
Timer? _scrollDownDebounce;
Future<void> Function(Tuple6<List<PlatformFile>, String, String, String?, int?, String?>)? sendFunc;
Future<void> Function(Tuple6<List<PlatformFile>, String, String, String?, int?, String?>, bool)? sendFunc;
bool isProcessingImage = false;
@override
@ -146,8 +146,8 @@ class ConversationViewController extends StatefulController with SingleGetTicker
}
}
Future<void> send(List<PlatformFile> attachments, String text, String subject, String? replyGuid, int? replyPart, String? effectId) async {
sendFunc?.call(Tuple6(attachments, text, subject, replyGuid, replyPart, effectId));
Future<void> send(List<PlatformFile> attachments, String text, String subject, String? replyGuid, int? replyPart, String? effectId, bool isAudioMessage) async {
sendFunc?.call(Tuple6(attachments, text, subject, replyGuid, replyPart, effectId), isAudioMessage);
}
void queueImage(Tuple4<Attachment, PlatformFile, BuildContext, Completer<Uint8List>> item) {