From 3022e35f35ab9b20df9a7be07f8de639cceb57d0 Mon Sep 17 00:00:00 2001 From: ChuangWei Ma <39935368+chungwwei@users.noreply.github.com> Date: Thu, 7 Jul 2022 13:56:30 -0400 Subject: [PATCH] feat: chat-mode via overlay (#538) * feat: chat-mode via dialog * feat: use overlay * chore: resolve warning * fix: logic fix and allows startswith filtering * chore: code tidy * chore: bump sdk to at least 2.17.0 for enhance enum feature * chore: use enum instead of class --- lib/components/message_input.dart | 96 +++++++++++++++++++++++++++++++ lib/models/chat_mode.dart | 30 ++++++++++ pubspec.yaml | 2 +- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 lib/models/chat_mode.dart diff --git a/lib/components/message_input.dart b/lib/components/message_input.dart index 9ece26587..1ff6e78d6 100644 --- a/lib/components/message_input.dart +++ b/lib/components/message_input.dart @@ -1,9 +1,12 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:provider/provider.dart'; import 'package:rtchat/components/emote_picker.dart'; import 'package:rtchat/models/adapters/actions.dart'; import 'package:rtchat/models/channels.dart'; +import 'package:rtchat/models/chat_mode.dart'; import 'package:rtchat/models/commands.dart'; class MessageInputWidget extends StatefulWidget { @@ -20,6 +23,89 @@ class _MessageInputWidgetState extends State { final _chatInputFocusNode = FocusNode(); var _isEmotePickerVisible = false; + OverlayEntry? entry; + + @override + void initState() { + super.initState(); + } + + bool startsWithPossibleCommands(String text) { + if (text == "" || text.isEmpty) { + return false; + } + for (final mode in ChatMode.values) { + if (mode.title.startsWith(text)) { + return true; + } + } + return false; + } + + void hideOverlay() { + entry?.remove(); + entry = null; + } + + void showOverlay(String text) { + // remove existing overlay, bc user can contiunously type a prefix string that matches a command + hideOverlay(); + + final overlay = Overlay.of(context)!; + + // the renderbox of this widget + final renderBox = context.findRenderObject() as RenderBox; + final size = renderBox.size; + + final offset = renderBox.localToGlobal(Offset.zero); + final lst = + ChatMode.values.where((element) => element.title.startsWith(text)); + + // None to show + if (lst.isEmpty) { + hideOverlay(); + return; + } + + final lstSize = lst.length; + const listTileSize = 75; // the roughly size of a listTile + final shiftUp = min(lstSize * listTileSize, 300); + + entry = OverlayEntry(builder: (context) { + return Positioned( + left: offset.dx, + top: offset.dy - shiftUp, + width: size.width, + child: Material( + child: SizedBox( + height: shiftUp.toDouble(), + child: ListView( + padding: EdgeInsets.zero, + shrinkWrap: true, + primary: false, + children: lst.map((e) { + return ListTile( + title: Text(e.title), + subtitle: Text(e.subtitle), + onTap: () { + _textEditingController.text = e.title; + hideOverlay(); + // move cursor position + _textEditingController.selection = + TextSelection.fromPosition(TextPosition( + offset: _textEditingController.text.length)); + }, + ); + }).toList(), + ), + ), + ), + ); + }); + + overlay.insert(entry!); + } + void sendMessage(String value) async { value = value.trim(); if (value.isEmpty) { @@ -83,6 +169,11 @@ class _MessageInputWidgetState extends State { @override Widget build(BuildContext context) { + // remove overlay if keyboard is not visible + if (MediaQuery.of(context).viewInsets.bottom == 0) { + hideOverlay(); + } + return Material( child: Column(children: [ Padding( @@ -134,6 +225,11 @@ class _MessageInputWidgetState extends State { border: InputBorder.none, hintText: "Send a message..."), onChanged: (text) { + if (startsWithPossibleCommands(text)) { + showOverlay(text); + } else { + hideOverlay(); + } final filtered = text.replaceAll('\n', ' '); if (filtered == text) { return; diff --git a/lib/models/chat_mode.dart b/lib/models/chat_mode.dart new file mode 100644 index 000000000..b7c5b2a3a --- /dev/null +++ b/lib/models/chat_mode.dart @@ -0,0 +1,30 @@ +enum ChatMode { + followers( + title: "/followers", + subtitle: + "Restrict the chat to followers-only mode; optionally, specify a time duration (e.g., 30 minutes, 1 week)"), + followersoff(title: "/followersoff", subtitle: "Disable followers-only mode"), + subscribers(title: "/subscribers", subtitle: "Restrict Chat to subscribers"), + subscribersoff( + title: "/subscribersoff", subtitle: "Turn off subscribers-only mode"), + uniquechat( + title: "/uniquechat", + subtitle: "Prevent users from sending duplicate messages in Chat"), + uniquechatoff(title: "/uniquechatoff", subtitle: "Turn off unique-chat mode"), + emoteonly( + title: "/emoteonly", + subtitle: "Users can only send emotes in their messages"), + emoteonlyoff(title: "/emoteonlyoff", subtitle: "Disable emotes only mode"), + slow( + title: "/slow", + subtitle: "Limit the rate at which users can send messages"), + slowoff(title: "/slowoff", subtitle: "Disable slow mode"); + + const ChatMode({ + required this.title, + required this.subtitle, + }); + + final String title; + final String subtitle; +} diff --git a/pubspec.yaml b/pubspec.yaml index 5fc38b69c..b91040c97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" version: 1.0.0+4 environment: - sdk: ">=2.16.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: