mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-25 21:16:40 +10:00
Fix dev
rebase dev over main and resolve merge conflicts
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## BLE Frames & Protocol Notes
|
## BLE Frames & Protocol Notes
|
||||||
- Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`.
|
- Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`.
|
||||||
- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`.
|
- Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`.
|
||||||
- Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`.
|
- Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`.
|
||||||
- Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57.
|
- Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57.
|
||||||
- Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25.
|
- Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ lib/
|
|||||||
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
||||||
|
|
||||||
### Device Discovery
|
### Device Discovery
|
||||||
- Scans for devices with name prefix `MeshCore-`
|
- Scans for devices with known name prefixes
|
||||||
- Filters by `platformName` or `advertisementData.advName`
|
- Filters by `platformName` or `advertisementData.advName`
|
||||||
|
|
||||||
### Connection States
|
### Connection States
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# How to contribute to Meshcore Open
|
||||||
|
|
||||||
|
Before submitting any pull requests (PR), please review the following information.
|
||||||
|
|
||||||
|
Unsolicited PRs without previous discussion or open issues may be
|
||||||
|
rejected. As may changes that are too broad (i.e. 100 files changed) or that
|
||||||
|
cover too many separate changes. If the changes are clearly AI generated they
|
||||||
|
may also be rejected. [See more](#ai-use)
|
||||||
|
|
||||||
|
## First Step Checklist
|
||||||
|
|
||||||
|
### **Did you find a bug?**
|
||||||
|
|
||||||
|
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/zjs81/meshcore-open/issues).
|
||||||
|
|
||||||
|
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/zjs81/meshcore-open/issues/new).
|
||||||
|
Be sure to include a **title and clear description**, as much relevant
|
||||||
|
information as possible, and a **code sample** or an **executable test case**
|
||||||
|
demonstrating the expected behavior that is not occurring. You can also include
|
||||||
|
screenshots or video.
|
||||||
|
|
||||||
|
* DO NOT start work and submit a PR at this time, please discuss the issue and
|
||||||
|
your implementation plan first.
|
||||||
|
|
||||||
|
### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
|
||||||
|
|
||||||
|
Changes that are cosmetic in nature and do not add anything substantial to the
|
||||||
|
stability, functionality, or testability of the application will generally not
|
||||||
|
be accepted.
|
||||||
|
|
||||||
|
### **Do you intend to add a new feature or change an existing one?**
|
||||||
|
|
||||||
|
* Suggest your change in a new issue as a feature request.
|
||||||
|
|
||||||
|
* DO NOT start work and submit a PR at this time, please discuss the change and
|
||||||
|
your implementation plan first.
|
||||||
|
|
||||||
|
* After it is generally decided that the feature or change fits the goals of the
|
||||||
|
project you can start work or open a PR if you have already started.
|
||||||
|
|
||||||
|
## Submitting your patch
|
||||||
|
|
||||||
|
* All changes should be based on the `dev` branch. When creating your PR please
|
||||||
|
be sure to change the target to merge into dev, and when starting work on a new
|
||||||
|
branch be sure to start on latest `dev`.
|
||||||
|
|
||||||
|
* Ensure the PR description clearly describes the problem and solution. Include
|
||||||
|
the relevant issue number if applicable.
|
||||||
|
|
||||||
|
* The PR should contain **one commit** only, the commit message should have a
|
||||||
|
clear title followed by a new line and then brief description if needed. PR with
|
||||||
|
multiple commits will be squashed into one before merging if required. See
|
||||||
|
[Git Mastery](https://git-mastery.org/lessons/commitMessage/) for more
|
||||||
|
information on good commit messages.
|
||||||
|
|
||||||
|
* **Before committing changes** on your branch, be sure to run both
|
||||||
|
`dart format .` and `flutter analyze`. The continuous development checks will
|
||||||
|
fail if issues here are not addressed before hand.
|
||||||
|
|
||||||
|
## AI-use
|
||||||
|
|
||||||
|
Everyone loves some help, AI agents are a tool in many of our belts. The project
|
||||||
|
is not anti-AI.
|
||||||
|
|
||||||
|
There are some limits to acceptable use however. Generally:
|
||||||
|
|
||||||
|
* All code generated by AI should be thoroughly reviewed by the contributor.
|
||||||
|
* The changes should be tightly controlled to not change anything out of scope
|
||||||
|
for the patch, bug fix, etc.
|
||||||
|
* The contributor should have a good understanding of what the code does and how
|
||||||
|
the application works in order to effectively be able to manage the agent.
|
||||||
@@ -150,7 +150,8 @@ lib/
|
|||||||
├── main.dart # App entry point
|
├── main.dart # App entry point
|
||||||
├── connector/
|
├── connector/
|
||||||
│ ├── meshcore_connector.dart # BLE communication & state management
|
│ ├── meshcore_connector.dart # BLE communication & state management
|
||||||
│ └── meshcore_protocol.dart # Protocol definitions & frame parsing
|
│ ├── meshcore_protocol.dart # Protocol definitions & frame parsing
|
||||||
|
│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!)
|
||||||
├── screens/
|
├── screens/
|
||||||
│ ├── scanner_screen.dart # Device scanning (home screen)
|
│ ├── scanner_screen.dart # Device scanning (home screen)
|
||||||
│ ├── contacts_screen.dart # Contact list
|
│ ├── contacts_screen.dart # Contact list
|
||||||
@@ -184,7 +185,15 @@ lib/
|
|||||||
|
|
||||||
### Device Discovery
|
### Device Discovery
|
||||||
|
|
||||||
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
|
||||||
|
- `MeshCore-`
|
||||||
|
- `Whisper-`
|
||||||
|
- `WisCore-`
|
||||||
|
- `HT-`
|
||||||
|
- `LowMesh_MC_`
|
||||||
|
|
||||||
|
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
|
||||||
|
|
||||||
|
|
||||||
### Message Format
|
### Message Format
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u
|
|||||||
|
|
||||||
### Connection Flow
|
### Connection Flow
|
||||||
|
|
||||||
1. **Scan** for devices with name prefix `MeshCore-`
|
1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||||
|
- `MeshCore-`
|
||||||
|
- `Whisper-`
|
||||||
|
- `WisCore-`
|
||||||
|
- `HT-`
|
||||||
|
- `LowMesh_MC_`
|
||||||
2. **Connect** with 15-second timeout
|
2. **Connect** with 15-second timeout
|
||||||
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
|
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
|
||||||
4. **Discover services** and locate NUS characteristics
|
4. **Discover services** and locate NUS characteristics
|
||||||
|
|||||||
@@ -49,7 +49,12 @@ enum MeshCoreConnectionState {
|
|||||||
|
|
||||||
## BLE Connection Lifecycle
|
## BLE Connection Lifecycle
|
||||||
|
|
||||||
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
|
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||||
|
- `MeshCore-`
|
||||||
|
- `Whisper-`
|
||||||
|
- `WisCore-`
|
||||||
|
- `HT-`
|
||||||
|
- `LowMesh_MC_`
|
||||||
2. **Connect** with 15-second timeout
|
2. **Connect** with 15-second timeout
|
||||||
3. **Request MTU** 185 bytes (non-web only)
|
3. **Request MTU** 185 bytes (non-web only)
|
||||||
4. **Discover services** and locate NUS
|
4. **Discover services** and locate NUS
|
||||||
|
|||||||
@@ -3976,11 +3976,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
tag: 'Connector',
|
tag: 'Connector',
|
||||||
);
|
);
|
||||||
|
|
||||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
// Preserve user-selected path settings and previously known GPS when
|
||||||
|
// refreshed frames omit coordinates (lat/lon encoded as 0,0).
|
||||||
_contacts[existingIndex] = contact.copyWith(
|
_contacts[existingIndex] = contact.copyWith(
|
||||||
lastMessageAt: mergedLastMessageAt,
|
lastMessageAt: mergedLastMessageAt,
|
||||||
pathOverride: existing.pathOverride, // Preserve user's path choice
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
pathOverrideBytes: existing.pathOverrideBytes,
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
latitude: contact.latitude ?? existing.latitude,
|
||||||
|
longitude: contact.longitude ?? existing.longitude,
|
||||||
);
|
);
|
||||||
|
|
||||||
appLogger.info(
|
appLogger.info(
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class MeshCoreUuids {
|
|||||||
"MeshCore-",
|
"MeshCore-",
|
||||||
"Whisper-",
|
"Whisper-",
|
||||||
"WisCore-",
|
"WisCore-",
|
||||||
|
"Seeed",
|
||||||
|
"Lilygo",
|
||||||
"HT-",
|
"HT-",
|
||||||
|
"LowMesh_MC_",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
class GifHelper {
|
||||||
|
/// Parse a known GIF format, which can be any of:
|
||||||
|
/// g:GIFID
|
||||||
|
/// https://media.giphy.com/media/GIFID/giphy.gif
|
||||||
|
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
|
||||||
|
///
|
||||||
|
/// GIFID is a Giphy GIF ID. The https:// is optional (and
|
||||||
|
/// can also be http://). The giphy.com/gifs form can also
|
||||||
|
/// include a trailing slash.
|
||||||
|
///
|
||||||
|
/// Returns null if text is not a valid GIF format
|
||||||
|
static String? parseGif(String text) {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||||
|
if (match != null) {
|
||||||
|
return match.group(1);
|
||||||
|
}
|
||||||
|
final directUrlMatch = RegExp(
|
||||||
|
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
|
||||||
|
).firstMatch(trimmed);
|
||||||
|
if (directUrlMatch != null) {
|
||||||
|
return directUrlMatch.group(1);
|
||||||
|
}
|
||||||
|
// Giphy understands page URLs with just the ID, or any string and a
|
||||||
|
// dash before the ID, and redirects to a page with a dash-separated
|
||||||
|
// title, a dash, and the ID. IDs in this form *probably* can't
|
||||||
|
// contain dashes.
|
||||||
|
final pageMatch = RegExp(
|
||||||
|
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
|
||||||
|
).firstMatch(trimmed);
|
||||||
|
return pageMatch?.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a GIF in a format that parseGif() can parse.
|
||||||
|
static String encodeGif(String gifId) {
|
||||||
|
return 'g:$gifId';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,4 +109,9 @@ class ReactionHelper {
|
|||||||
|
|
||||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a reaction message that parseReaction() can parse.
|
||||||
|
static String encodeReaction(String hash, String emojiIndex) {
|
||||||
|
return 'r:$hash:$emojiIndex';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2098,6 +2098,7 @@
|
|||||||
"translation_translateTo": "Fordítás {language}-ra",
|
"translation_translateTo": "Fordítás {language}-ra",
|
||||||
"translation_translationOptions": "Fordítási lehetőségek",
|
"translation_translationOptions": "Fordítási lehetőségek",
|
||||||
"translation_systemLanguage": "Rendszer nyelvé",
|
"translation_systemLanguage": "Rendszer nyelvé",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
|
||||||
"repeater_cliQuickClockSync": "Óra szinkronizálás",
|
"repeater_cliQuickClockSync": "Óra szinkronizálás",
|
||||||
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat"
|
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat"
|
||||||
}
|
}
|
||||||
@@ -3677,7 +3677,7 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).';
|
return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart';
|
|||||||
import '../utils/platform_info.dart';
|
import '../utils/platform_info.dart';
|
||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../helpers/gif_helper.dart';
|
||||||
import '../helpers/reaction_helper.dart';
|
import '../helpers/reaction_helper.dart';
|
||||||
import '../helpers/utf8_length_limiter.dart';
|
import '../helpers/utf8_length_limiter.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
@@ -355,7 +356,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final settingsService = context.watch<AppSettingsService>();
|
final settingsService = context.watch<AppSettingsService>();
|
||||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||||
final isOutgoing = message.isOutgoing;
|
final isOutgoing = message.isOutgoing;
|
||||||
final gifId = _parseGifId(message.text);
|
final gifId = GifHelper.parseGif(message.text);
|
||||||
final poi = _parsePoiMessage(message.text);
|
final poi = _parsePoiMessage(message.text);
|
||||||
final translatedDisplayText =
|
final translatedDisplayText =
|
||||||
message.translatedText != null &&
|
message.translatedText != null &&
|
||||||
@@ -699,7 +700,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
||||||
|
|
||||||
final gifId = _parseGifId(replyText);
|
final gifId = GifHelper.parseGif(replyText);
|
||||||
final poi = _parsePoiMessage(replyText);
|
final poi = _parsePoiMessage(replyText);
|
||||||
|
|
||||||
Widget contentPreview;
|
Widget contentPreview;
|
||||||
@@ -811,12 +812,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _parseGifId(String text) {
|
|
||||||
final trimmed = text.trim();
|
|
||||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
|
||||||
return match?.group(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_PoiInfo? _parsePoiMessage(String text) {
|
_PoiInfo? _parsePoiMessage(String text) {
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
final match = RegExp(
|
final match = RegExp(
|
||||||
@@ -897,7 +892,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => GifPicker(
|
builder: (context) => GifPicker(
|
||||||
onGifSelected: (gifId) {
|
onGifSelected: (gifId) {
|
||||||
_textController.text = 'g:$gifId';
|
_textController.text = GifHelper.encodeGif(gifId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1053,7 +1048,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
child: ValueListenableBuilder<TextEditingValue>(
|
child: ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: _textController,
|
valueListenable: _textController,
|
||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
final gifId = _parseGifId(value.text);
|
final gifId = GifHelper.parseGif(value.text);
|
||||||
if (gifId != null) {
|
if (gifId != null) {
|
||||||
return Focus(
|
return Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -1322,7 +1317,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
message.senderName,
|
message.senderName,
|
||||||
message.text,
|
message.text,
|
||||||
);
|
);
|
||||||
final reactionText = 'r:$hash:$emojiIndex';
|
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
|
||||||
connector.sendChannelMessage(widget.channel, reactionText);
|
connector.sendChannelMessage(widget.channel, reactionText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart';
|
|||||||
import '../helpers/reaction_helper.dart';
|
import '../helpers/reaction_helper.dart';
|
||||||
import '../widgets/message_status_icon.dart';
|
import '../widgets/message_status_icon.dart';
|
||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
|
import '../helpers/gif_helper.dart';
|
||||||
import '../helpers/path_helper.dart';
|
import '../helpers/path_helper.dart';
|
||||||
import '../helpers/utf8_length_limiter.dart';
|
import '../helpers/utf8_length_limiter.dart';
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
@@ -523,7 +524,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: ValueListenableBuilder<TextEditingValue>(
|
child: ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: _textController,
|
valueListenable: _textController,
|
||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
final gifId = _parseGifId(value.text);
|
final gifId = GifHelper.parseGif(value.text);
|
||||||
if (gifId != null) {
|
if (gifId != null) {
|
||||||
return Focus(
|
return Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -601,19 +602,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _parseGifId(String text) {
|
|
||||||
final trimmed = text.trim();
|
|
||||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
|
||||||
return match?.group(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showGifPicker(BuildContext context) {
|
void _showGifPicker(BuildContext context) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => GifPicker(
|
builder: (context) => GifPicker(
|
||||||
onGifSelected: (gifId) {
|
onGifSelected: (gifId) {
|
||||||
_textController.text = 'g:$gifId';
|
_textController.text = GifHelper.encodeGif(gifId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1546,7 +1541,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
senderName,
|
senderName,
|
||||||
message.text,
|
message.text,
|
||||||
);
|
);
|
||||||
final reactionText = 'r:$hash:$emojiIndex';
|
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
|
||||||
connector.sendMessage(_resolveContact(connector), reactionText);
|
connector.sendMessage(_resolveContact(connector), reactionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1576,7 +1571,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||||
final isOutgoing = message.isOutgoing;
|
final isOutgoing = message.isOutgoing;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final gifId = _parseGifId(message.text);
|
final gifId = GifHelper.parseGif(message.text);
|
||||||
final poi = _parsePoiMessage(message.text);
|
final poi = _parsePoiMessage(message.text);
|
||||||
final isFailed = message.status == MessageStatus.failed;
|
final isFailed = message.status == MessageStatus.failed;
|
||||||
final bubbleColor = isFailed
|
final bubbleColor = isFailed
|
||||||
@@ -1850,12 +1845,6 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _parseGifId(String text) {
|
|
||||||
final trimmed = text.trim();
|
|
||||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
|
||||||
return match?.group(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_PoiInfo? _parsePoiMessage(String text) {
|
_PoiInfo? _parsePoiMessage(String text) {
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
final match = RegExp(
|
final match = RegExp(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:llamadart/llamadart.dart';
|
|||||||
|
|
||||||
import '../models/app_settings.dart';
|
import '../models/app_settings.dart';
|
||||||
import '../models/translation_support.dart';
|
import '../models/translation_support.dart';
|
||||||
|
import '../helpers/gif_helper.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import 'app_settings_service.dart';
|
import 'app_settings_service.dart';
|
||||||
import 'translation_file_store.dart';
|
import 'translation_file_store.dart';
|
||||||
@@ -509,8 +510,10 @@ class TranslationService extends ChangeNotifier {
|
|||||||
if (trimmed.isEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !(trimmed.startsWith('g:') ||
|
if (GifHelper.parseGif(trimmed) != null) {
|
||||||
trimmed.startsWith('m:') ||
|
return false;
|
||||||
|
}
|
||||||
|
return !(trimmed.startsWith('m:') ||
|
||||||
trimmed.startsWith('V1|') ||
|
trimmed.startsWith('V1|') ||
|
||||||
trimmed.startsWith('r:'));
|
trimmed.startsWith('r:'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import flutter_blue_plus_darwin
|
|||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import mobile_scanner
|
import mobile_scanner
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
@@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
|||||||
Reference in New Issue
Block a user