mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
fix #289
This commit is contained in:
parent
57334d9eee
commit
94f60b5806
10 changed files with 309 additions and 208 deletions
|
|
@ -5,7 +5,9 @@
|
||||||
- Support for groups
|
- Support for groups
|
||||||
- Edit & Delete messages
|
- Edit & Delete messages
|
||||||
- Switched to FFmpeg for improved video compression
|
- Switched to FFmpeg for improved video compression
|
||||||
|
- Create images using volume buttons
|
||||||
- Video max. length increased to 60 seconds
|
- Video max. length increased to 60 seconds
|
||||||
|
- New and improved emoji picker
|
||||||
- Removing audio after recording is possible
|
- Removing audio after recording is possible
|
||||||
- Edited image is now embedded into the video
|
- Edited image is now embedded into the video
|
||||||
- New context menu and other UI enhancements
|
- New context menu and other UI enhancements
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
seedColor: const Color(0xFF57CC99),
|
seedColor: const Color(0xFF57CC99),
|
||||||
|
surface: const Color.fromARGB(255, 20, 18, 23),
|
||||||
|
surfaceContainer: const Color.fromARGB(255, 33, 30, 39),
|
||||||
),
|
),
|
||||||
inputDecorationTheme: const InputDecorationTheme(
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,6 @@ class UserData {
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
bool storeMediaFilesInGallery = false;
|
bool storeMediaFilesInGallery = false;
|
||||||
|
|
||||||
List<String>? lastUsedEditorEmojis;
|
|
||||||
|
|
||||||
String? lastPlanBallance;
|
String? lastPlanBallance;
|
||||||
String? additionalUserInvites;
|
String? additionalUserInvites;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
||||||
)
|
)
|
||||||
..storeMediaFilesInGallery =
|
..storeMediaFilesInGallery =
|
||||||
json['storeMediaFilesInGallery'] as bool? ?? false
|
json['storeMediaFilesInGallery'] as bool? ?? false
|
||||||
..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List<dynamic>?)
|
|
||||||
?.map((e) => e as String)
|
|
||||||
.toList()
|
|
||||||
..lastPlanBallance = json['lastPlanBallance'] as String?
|
..lastPlanBallance = json['lastPlanBallance'] as String?
|
||||||
..additionalUserInvites = json['additionalUserInvites'] as String?
|
..additionalUserInvites = json['additionalUserInvites'] as String?
|
||||||
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
|
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
|
||||||
|
|
@ -93,7 +90,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'preSelectedEmojies': instance.preSelectedEmojies,
|
'preSelectedEmojies': instance.preSelectedEmojies,
|
||||||
'autoDownloadOptions': instance.autoDownloadOptions,
|
'autoDownloadOptions': instance.autoDownloadOptions,
|
||||||
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
||||||
'lastUsedEditorEmojis': instance.lastUsedEditorEmojis,
|
|
||||||
'lastPlanBallance': instance.lastPlanBallance,
|
'lastPlanBallance': instance.lastPlanBallance,
|
||||||
'additionalUserInvites': instance.additionalUserInvites,
|
'additionalUserInvites': instance.additionalUserInvites,
|
||||||
'tutorialDisplayed': instance.tutorialDisplayed,
|
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class TextLayer extends StatefulWidget {
|
||||||
class _TextViewState extends State<TextLayer> {
|
class _TextViewState extends State<TextLayer> {
|
||||||
double initialRotation = 0;
|
double initialRotation = 0;
|
||||||
bool deleteLayer = false;
|
bool deleteLayer = false;
|
||||||
|
double localBottom = 0;
|
||||||
bool isDeleted = false;
|
bool isDeleted = false;
|
||||||
bool elementIsScaled = false;
|
bool elementIsScaled = false;
|
||||||
final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey
|
final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey
|
||||||
|
|
@ -35,9 +36,19 @@ class _TextViewState extends State<TextLayer> {
|
||||||
|
|
||||||
textController.text = widget.layerData.text;
|
textController.text = widget.layerData.text;
|
||||||
|
|
||||||
if (widget.layerData.offset.dy == 0) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// Set the initial offset to the center of the screen
|
final mq = MediaQuery.of(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
final globalDesiredBottom = mq.viewInsets.bottom + mq.viewPadding.bottom;
|
||||||
|
final parentBox = context.findRenderObject() as RenderBox?;
|
||||||
|
if (parentBox != null) {
|
||||||
|
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
|
||||||
|
final screenHeight = mq.size.height;
|
||||||
|
localBottom = (screenHeight - globalDesiredBottom) -
|
||||||
|
parentTopGlobal -
|
||||||
|
(parentBox.size.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.layerData.offset.dy == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.layerData.offset = Offset(
|
widget.layerData.offset = Offset(
|
||||||
0,
|
0,
|
||||||
|
|
@ -47,17 +58,20 @@ class _TextViewState extends State<TextLayer> {
|
||||||
);
|
);
|
||||||
textController.text = widget.layerData.text;
|
textController.text = widget.layerData.text;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.layerData.isDeleted) return Container();
|
if (widget.layerData.isDeleted) return Container();
|
||||||
|
|
||||||
|
final bottom = MediaQuery.of(context).viewInsets.bottom +
|
||||||
|
MediaQuery.of(context).viewPadding.bottom;
|
||||||
|
|
||||||
if (widget.layerData.isEditing) {
|
if (widget.layerData.isEditing) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom - 100,
|
bottom: bottom - localBottom,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
|
||||||
|
|
@ -1,107 +1,83 @@
|
||||||
import 'dart:async';
|
import 'dart:io';
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
|
||||||
import 'package:twonly/src/views/camera/image_editor/data/data.dart';
|
|
||||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||||
|
|
||||||
class Emojis extends StatefulWidget {
|
class EmojiPickerBottom extends StatelessWidget {
|
||||||
const Emojis({super.key});
|
const EmojiPickerBottom({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<Emojis> createState() => _EmojisState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EmojisState extends State<Emojis> {
|
|
||||||
List<String> lastUsed = emojis;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
unawaited(initAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
|
||||||
setState(() {
|
|
||||||
lastUsed = gUser.lastUsedEditorEmojis ?? [];
|
|
||||||
lastUsed.addAll(emojis);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> selectEmojis(String emoji) async {
|
|
||||||
await updateUserdata((user) {
|
|
||||||
if (user.lastUsedEditorEmojis == null) {
|
|
||||||
user.lastUsedEditorEmojis = [emoji];
|
|
||||||
} else {
|
|
||||||
if (user.lastUsedEditorEmojis!.contains(emoji)) {
|
|
||||||
user.lastUsedEditorEmojis!.remove(emoji);
|
|
||||||
}
|
|
||||||
user.lastUsedEditorEmojis!.insert(0, emoji);
|
|
||||||
if (user.lastUsedEditorEmojis!.length > 12) {
|
|
||||||
user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12);
|
|
||||||
}
|
|
||||||
user.lastUsedEditorEmojis!.toSet().toList();
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(
|
|
||||||
context,
|
|
||||||
EmojiLayerData(
|
|
||||||
text: emoji,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
height: 400,
|
height: 450,
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(32),
|
topLeft: Radius.circular(32),
|
||||||
topRight: Radius.circular(32),
|
topRight: Radius.circular(32),
|
||||||
),
|
),
|
||||||
color: Colors.black,
|
color: context.color.surfaceContainer,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 10.9,
|
blurRadius: 10.9,
|
||||||
color: Color.fromRGBO(0, 0, 0, 0.1),
|
color: context.color.surfaceContainer.withAlpha(25),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
|
||||||
Container(
|
Container(
|
||||||
height: 315,
|
margin: const EdgeInsets.all(30),
|
||||||
padding: EdgeInsets.zero,
|
decoration: BoxDecoration(
|
||||||
child: GridView(
|
borderRadius: BorderRadius.circular(32),
|
||||||
shrinkWrap: true,
|
color: Colors.grey,
|
||||||
physics: const ClampingScrollPhysics(),
|
),
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
height: 3,
|
||||||
maxCrossAxisExtent: 60,
|
width: 60,
|
||||||
),
|
),
|
||||||
children: lastUsed.map((String emoji) {
|
Expanded(
|
||||||
return GridTile(
|
child: EmojiPicker(
|
||||||
child: GestureDetector(
|
onEmojiSelected: (category, emoji) {
|
||||||
onTap: () async {
|
Navigator.pop(
|
||||||
await selectEmojis(emoji);
|
context,
|
||||||
},
|
EmojiLayerData(
|
||||||
child: Container(
|
text: emoji.emoji,
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
emoji,
|
|
||||||
style: const TextStyle(fontSize: 35),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
},
|
||||||
|
// textEditingController: _textFieldController,
|
||||||
|
config: Config(
|
||||||
|
height: 400,
|
||||||
|
locale: Localizations.localeOf(context),
|
||||||
|
viewOrderConfig: const ViewOrderConfig(
|
||||||
|
top: EmojiPickerItem.searchBar,
|
||||||
|
// middle: EmojiPickerItem.emojiView,
|
||||||
|
bottom: EmojiPickerItem.categoryBar,
|
||||||
|
),
|
||||||
|
emojiTextStyle:
|
||||||
|
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
|
||||||
|
emojiViewConfig: EmojiViewConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
),
|
||||||
|
searchViewConfig: SearchViewConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
buttonIconColor: Colors.white,
|
||||||
|
),
|
||||||
|
categoryViewConfig: CategoryViewConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
dividerColor: Colors.white,
|
||||||
|
indicatorColor: context.color.primary,
|
||||||
|
iconColorSelected: context.color.primary,
|
||||||
|
iconColor: context.color.secondary,
|
||||||
|
),
|
||||||
|
bottomActionBarConfig: BottomActionBarConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
buttonColor: context.color.surfaceContainer,
|
||||||
|
buttonIconColor: context.color.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return const Emojis();
|
return const EmojiPickerBottom();
|
||||||
},
|
},
|
||||||
) as Layer?;
|
) as Layer?;
|
||||||
if (layer == null) return;
|
if (layer == null) return;
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,10 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
import 'package:twonly/src/services/api/messages.dart';
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||||
import 'package:twonly/src/views/components/flame.dart';
|
import 'package:twonly/src/views/components/flame.dart';
|
||||||
|
|
@ -70,10 +69,8 @@ class ChatMessagesView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatMessagesViewState extends State<ChatMessagesView> {
|
class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
TextEditingController newMessageController = TextEditingController();
|
|
||||||
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
||||||
late Group group;
|
late Group group;
|
||||||
String currentInputText = '';
|
|
||||||
late StreamSubscription<Group?> userSub;
|
late StreamSubscription<Group?> userSub;
|
||||||
late StreamSubscription<List<Message>> messageSub;
|
late StreamSubscription<List<Message>> messageSub;
|
||||||
StreamSubscription<List<GroupHistory>>? groupActionsSub;
|
StreamSubscription<List<GroupHistory>>? groupActionsSub;
|
||||||
|
|
@ -267,21 +264,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
|
||||||
if (newMessageController.text == '') return;
|
|
||||||
|
|
||||||
await insertAndSendTextMessage(
|
|
||||||
group.groupId,
|
|
||||||
newMessageController.text,
|
|
||||||
quotesMessage?.messageId,
|
|
||||||
);
|
|
||||||
|
|
||||||
newMessageController.clear();
|
|
||||||
currentInputText = '';
|
|
||||||
quotesMessage = null;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> scrollToMessage(String messageId) async {
|
Future<void> scrollToMessage(String messageId) async {
|
||||||
final index = messages.indexWhere(
|
final index = messages.indexWhere(
|
||||||
(x) => x.isMessage && x.message!.messageId == messageId,
|
(x) => x.isMessage && x.message!.messageId == messageId,
|
||||||
|
|
@ -464,100 +446,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!group.leftGroup)
|
if (!group.leftGroup)
|
||||||
Padding(
|
MessageInput(
|
||||||
padding: const EdgeInsets.only(
|
group: group,
|
||||||
bottom: 30,
|
quotesMessage: quotesMessage,
|
||||||
left: 20,
|
textFieldFocus: textFieldFocus,
|
||||||
right: 20,
|
onMessageSend: () {
|
||||||
top: 10,
|
setState(() {
|
||||||
),
|
quotesMessage = null;
|
||||||
child: Row(
|
});
|
||||||
children: [
|
},
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
color: Colors.grey,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const FaIcon(FontAwesomeIcons.faceSmile),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: newMessageController,
|
|
||||||
focusNode: textFieldFocus,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
maxLines: 4,
|
|
||||||
minLines: 1,
|
|
||||||
onChanged: (value) {
|
|
||||||
currentInputText = value;
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
onSubmitted: (_) {
|
|
||||||
_sendMessage();
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.lang.chatListDetailInput,
|
|
||||||
// contentPadding: const EdgeInsets.symmetric(
|
|
||||||
// horizontal: 20,
|
|
||||||
// vertical: 10,
|
|
||||||
// ),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
borderSide: const BorderSide(
|
|
||||||
color: Colors.grey,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (currentInputText != '')
|
|
||||||
IconButton(
|
|
||||||
padding: const EdgeInsets.all(15),
|
|
||||||
icon: const FaIcon(
|
|
||||||
FontAwesomeIcons.solidPaperPlane,
|
|
||||||
),
|
|
||||||
onPressed: _sendMessage,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
|
||||||
padding: const EdgeInsets.all(15),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return CameraSendToView(widget.group);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return const Emojis();
|
return const EmojiPickerBottom();
|
||||||
},
|
},
|
||||||
) as EmojiLayerData?;
|
) as EmojiLayerData?;
|
||||||
if (layer == null) return;
|
if (layer == null) return;
|
||||||
|
|
|
||||||
216
lib/src/views/chats/chat_messages_components/message_input.dart
Normal file
216
lib/src/views/chats/chat_messages_components/message_input.dart
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||||
|
|
||||||
|
class MessageInput extends StatefulWidget {
|
||||||
|
const MessageInput({
|
||||||
|
required this.group,
|
||||||
|
required this.quotesMessage,
|
||||||
|
required this.textFieldFocus,
|
||||||
|
required this.onMessageSend,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Group group;
|
||||||
|
final FocusNode textFieldFocus;
|
||||||
|
final Message? quotesMessage;
|
||||||
|
final VoidCallback onMessageSend;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MessageInput> createState() => _MessageInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageInputState extends State<MessageInput> {
|
||||||
|
late final TextEditingController _textFieldController;
|
||||||
|
final bool isApple = Platform.isIOS;
|
||||||
|
bool _emojiShowing = false;
|
||||||
|
|
||||||
|
Future<void> _sendMessage() async {
|
||||||
|
if (_textFieldController.text == '') return;
|
||||||
|
|
||||||
|
await insertAndSendTextMessage(
|
||||||
|
widget.group.groupId,
|
||||||
|
_textFieldController.text,
|
||||||
|
widget.quotesMessage?.messageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
_textFieldController.clear();
|
||||||
|
_emojiShowing = false;
|
||||||
|
widget.onMessageSend();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_textFieldController = TextEditingController();
|
||||||
|
widget.textFieldFocus.addListener(_handleTextFocusChange);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.textFieldFocus.removeListener(_handleTextFocusChange);
|
||||||
|
widget.textFieldFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTextFocusChange() {
|
||||||
|
if (widget.textFieldFocus.hasFocus) {
|
||||||
|
setState(() {
|
||||||
|
_emojiShowing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 10,
|
||||||
|
left: 10,
|
||||||
|
top: 10,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_emojiShowing = !_emojiShowing;
|
||||||
|
if (_emojiShowing) {
|
||||||
|
widget.textFieldFocus.unfocus();
|
||||||
|
} else {
|
||||||
|
widget.textFieldFocus.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
left: 12,
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
size: 20,
|
||||||
|
_emojiShowing
|
||||||
|
? FontAwesomeIcons.keyboard
|
||||||
|
: FontAwesomeIcons.faceSmile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _textFieldController,
|
||||||
|
focusNode: widget.textFieldFocus,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
maxLines: 4,
|
||||||
|
minLines: 1,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onSubmitted: (_) {
|
||||||
|
_sendMessage();
|
||||||
|
},
|
||||||
|
style: const TextStyle(fontSize: 17),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: context.lang.chatListDetailInput,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_textFieldController.text != '')
|
||||||
|
IconButton(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
icon: FaIcon(
|
||||||
|
color: context.color.primary,
|
||||||
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
|
),
|
||||||
|
onPressed: _sendMessage,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return CameraSendToView(widget.group);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !_emojiShowing,
|
||||||
|
child: EmojiPicker(
|
||||||
|
textEditingController: _textFieldController,
|
||||||
|
onEmojiSelected: (category, emoji) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onBackspacePressed: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
config: Config(
|
||||||
|
height: 300,
|
||||||
|
locale: Localizations.localeOf(context),
|
||||||
|
viewOrderConfig: const ViewOrderConfig(
|
||||||
|
top: EmojiPickerItem.searchBar,
|
||||||
|
// middle: EmojiPickerItem.emojiView,
|
||||||
|
bottom: EmojiPickerItem.categoryBar,
|
||||||
|
),
|
||||||
|
emojiTextStyle:
|
||||||
|
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
|
||||||
|
emojiViewConfig: EmojiViewConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
),
|
||||||
|
searchViewConfig: SearchViewConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
buttonIconColor: Colors.white,
|
||||||
|
),
|
||||||
|
categoryViewConfig: CategoryViewConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
dividerColor: Colors.white,
|
||||||
|
indicatorColor: context.color.primary,
|
||||||
|
iconColorSelected: context.color.primary,
|
||||||
|
iconColor: context.color.secondary,
|
||||||
|
),
|
||||||
|
bottomActionBarConfig: BottomActionBarConfig(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
buttonColor: context.color.surfaceContainer,
|
||||||
|
buttonIconColor: context.color.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue