Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b841d3f3e4 | ||
|
|
eca77ece81 | ||
|
|
3462de1eee | ||
|
|
f66118f5bb | ||
|
|
3ef540ae37 | ||
|
|
e0edcc89ac | ||
|
|
dc52092654 |
@@ -15,6 +15,12 @@ const double _chatSidebarTileExtent = 92;
|
||||
const double _chatSidebarCompactTileExtent = 76;
|
||||
const double _chatSidebarCompactHeight = 84;
|
||||
|
||||
typedef ChatMessageSender =
|
||||
Future<void> Function({
|
||||
required String message,
|
||||
required rust.BridgeMessageTarget target,
|
||||
});
|
||||
|
||||
/// One chat/activity message shown in the chat hub.
|
||||
class ChatEntry {
|
||||
/// Construct a chat entry.
|
||||
@@ -497,7 +503,7 @@ String chatInputPlaceholder(
|
||||
case rust.BridgeMessageTarget_Client():
|
||||
return 'Message $clientName...';
|
||||
case rust.BridgeMessageTarget_Poke():
|
||||
return 'Poke message...';
|
||||
return 'Poke message optional...';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +519,16 @@ bool canSendToChatTarget(
|
||||
}
|
||||
}
|
||||
|
||||
bool canSendChatMessage(
|
||||
rust.BridgeMessageTarget target,
|
||||
BigInt? currentChannelId,
|
||||
String text,
|
||||
) {
|
||||
if (!canSendToChatTarget(target, currentChannelId)) return false;
|
||||
if (target is rust.BridgeMessageTarget_Poke) return true;
|
||||
return text.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
String? chatSendBlockedReason(
|
||||
rust.BridgeMessageTarget target,
|
||||
BigInt? currentChannelId,
|
||||
@@ -1066,6 +1082,7 @@ class ChatDetailView extends StatefulWidget {
|
||||
this.messageMaxWidth,
|
||||
this.restoredDraft,
|
||||
this.onDraftChanged,
|
||||
this.sendChatMessage,
|
||||
});
|
||||
|
||||
/// Chat target displayed by this detail view.
|
||||
@@ -1101,6 +1118,9 @@ class ChatDetailView extends StatefulWidget {
|
||||
/// Called with the current draft text whenever the target changes or the widget is about to be replaced.
|
||||
final ValueChanged<String>? onDraftChanged;
|
||||
|
||||
/// Sends a chat message. Defaults to the Rust bridge send path.
|
||||
final ChatMessageSender? sendChatMessage;
|
||||
|
||||
@override
|
||||
State<ChatDetailView> createState() => _ChatDetailViewState();
|
||||
}
|
||||
@@ -1168,9 +1188,12 @@ class _ChatDetailViewState extends State<ChatDetailView> {
|
||||
|
||||
void _send() {
|
||||
final text = _textCtl.text.trim();
|
||||
if (text.isEmpty || !_canSend) return;
|
||||
if (!canSendChatMessage(widget.target, widget.currentChannelId, text)) {
|
||||
return;
|
||||
}
|
||||
_textCtl.clear();
|
||||
unawaited(rust.sendChatMessage(message: text, target: widget.target));
|
||||
final sendChatMessage = widget.sendChatMessage ?? rust.sendChatMessage;
|
||||
unawaited(sendChatMessage(message: text, target: widget.target));
|
||||
final ownId = widget.snapshot.ownClientId;
|
||||
setState(() {
|
||||
widget.messages.add(
|
||||
@@ -1223,6 +1246,9 @@ class _ChatDetailViewState extends State<ChatDetailView> {
|
||||
channelName: widget.channelName,
|
||||
clientName: widget.clientName,
|
||||
);
|
||||
final sendTooltip = widget.target is rust.BridgeMessageTarget_Poke
|
||||
? 'Poke'
|
||||
: 'Send';
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -1332,7 +1358,7 @@ class _ChatDetailViewState extends State<ChatDetailView> {
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: _send,
|
||||
tooltip: 'Send',
|
||||
tooltip: sendTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -60,4 +60,22 @@ void main() {
|
||||
expect(specifics['groupKey'], 'chanora.pokes');
|
||||
},
|
||||
);
|
||||
|
||||
test('show uses fallback body for empty poke messages', () async {
|
||||
final service = PokeNotificationService();
|
||||
|
||||
await service.show(
|
||||
senderName: 'Alice',
|
||||
message: ' ',
|
||||
senderId: BigInt.from(42),
|
||||
strength: rust.BridgePokeStrength.strong,
|
||||
);
|
||||
|
||||
final showCall = calls.singleWhere((call) => call.method == 'show');
|
||||
final arguments = Map<Object?, Object?>.from(showCall.arguments as Map);
|
||||
|
||||
expect(arguments['title'], 'Poke from Alice');
|
||||
expect(arguments['body'], 'Alice pokes you');
|
||||
expect(arguments['payload'], 'poke:42');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ void main() {
|
||||
channelName: '',
|
||||
clientName: 'Alpha',
|
||||
),
|
||||
'Poke message...',
|
||||
'Poke message optional...',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -737,6 +737,179 @@ void main() {
|
||||
refresh.dispose();
|
||||
});
|
||||
|
||||
test('evaluates target-aware chat message send policy', () {
|
||||
final clientTarget = rust.BridgeMessageTarget.client(BigInt.from(2));
|
||||
final pokeTarget = rust.BridgeMessageTarget.poke(BigInt.from(2));
|
||||
|
||||
expect(canSendChatMessage(pokeTarget, null, ''), isTrue);
|
||||
expect(canSendChatMessage(pokeTarget, null, ' '), isTrue);
|
||||
expect(canSendChatMessage(pokeTarget, null, 'wake up'), isTrue);
|
||||
|
||||
expect(
|
||||
canSendChatMessage(const rust.BridgeMessageTarget.server(), null, ''),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
canSendChatMessage(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
BigInt.from(10),
|
||||
'',
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(canSendChatMessage(clientTarget, null, ''), isFalse);
|
||||
expect(
|
||||
canSendChatMessage(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
null,
|
||||
'hello',
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
canSendChatMessage(
|
||||
const rust.BridgeMessageTarget.channel(),
|
||||
BigInt.from(10),
|
||||
'hello',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('poke detail sends an empty poke when the composer is empty', (
|
||||
tester,
|
||||
) async {
|
||||
String? sentMessage;
|
||||
rust.BridgeMessageTarget? sentTarget;
|
||||
final messages = <ChatEntry>[];
|
||||
final target = rust.BridgeMessageTarget.poke(BigInt.from(2));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: ChatDetailView(
|
||||
messages: messages,
|
||||
snapshot: snapshot(
|
||||
channels: const [],
|
||||
clients: [
|
||||
client(id: BigInt.one, name: 'Me', channelId: BigInt.zero),
|
||||
],
|
||||
),
|
||||
target: target,
|
||||
clientName: 'Alpha',
|
||||
currentChannelId: null,
|
||||
channelName: '',
|
||||
sendChatMessage: ({required message, required target}) async {
|
||||
sentMessage = message;
|
||||
sentTarget = target;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byTooltip('Poke'), findsOneWidget);
|
||||
expect(find.byTooltip('Send'), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Poke'));
|
||||
await tester.pump();
|
||||
|
||||
expect(sentMessage, '');
|
||||
expect(sentTarget, target);
|
||||
expect(messages, hasLength(1));
|
||||
expect(messages.single.isPoke, isTrue);
|
||||
expect(messages.single.message, '');
|
||||
expect(find.textContaining('You poked "Alpha"'), findsOneWidget);
|
||||
expect(find.byType(CircleAvatar), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('poke detail sends typed optional poke message', (tester) async {
|
||||
String? sentMessage;
|
||||
rust.BridgeMessageTarget? sentTarget;
|
||||
final messages = <ChatEntry>[];
|
||||
final target = rust.BridgeMessageTarget.poke(BigInt.from(2));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: ChatDetailView(
|
||||
messages: messages,
|
||||
snapshot: snapshot(
|
||||
channels: const [],
|
||||
clients: [
|
||||
client(id: BigInt.one, name: 'Me', channelId: BigInt.zero),
|
||||
],
|
||||
),
|
||||
target: target,
|
||||
clientName: 'Alpha',
|
||||
currentChannelId: null,
|
||||
channelName: '',
|
||||
sendChatMessage: ({required message, required target}) async {
|
||||
sentMessage = message;
|
||||
sentTarget = target;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'wake up');
|
||||
await tester.tap(find.byTooltip('Poke'));
|
||||
await tester.pump();
|
||||
|
||||
expect(sentMessage, 'wake up');
|
||||
expect(sentTarget, target);
|
||||
expect(messages.single.message, 'wake up');
|
||||
expect(
|
||||
find.textContaining('You poked "Alpha" with message: wake up'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('channel detail blocks empty sends with a joined channel', (
|
||||
tester,
|
||||
) async {
|
||||
var sendCount = 0;
|
||||
final messages = <ChatEntry>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: ChatDetailView(
|
||||
messages: messages,
|
||||
snapshot: snapshot(
|
||||
channels: [channel(BigInt.from(10), 'Lobby')],
|
||||
clients: [
|
||||
client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
|
||||
],
|
||||
),
|
||||
target: const rust.BridgeMessageTarget.channel(),
|
||||
clientName: '',
|
||||
currentChannelId: BigInt.from(10),
|
||||
channelName: 'Lobby',
|
||||
sendChatMessage: ({required message, required target}) async {
|
||||
sendCount++;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byTooltip('Send'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Send'));
|
||||
await tester.pump();
|
||||
|
||||
expect(sendCount, 0);
|
||||
expect(messages, isEmpty);
|
||||
});
|
||||
|
||||
test('blocks channel chat when no voice channel is joined', () {
|
||||
expect(
|
||||
canSendToChatTarget(const rust.BridgeMessageTarget.channel(), null),
|
||||
|
||||
@@ -174,6 +174,10 @@ fn normalize_channel_password(password: Option<String>) -> Option<String> {
|
||||
.filter(|p| !p.is_empty())
|
||||
}
|
||||
|
||||
fn should_dispatch_text_message(message: &str, target: &MessageTarget) -> bool {
|
||||
!message.trim().is_empty() || matches!(target, MessageTarget::Poke(_))
|
||||
}
|
||||
|
||||
/// The top-level Chanora session. Owns at most one active server
|
||||
/// connection (DEC-006).
|
||||
#[derive(Clone)]
|
||||
@@ -673,7 +677,7 @@ impl ChanoraSession {
|
||||
message: String,
|
||||
target: MessageTarget,
|
||||
) -> Result<(), CoreError> {
|
||||
if message.trim().is_empty() {
|
||||
if !should_dispatch_text_message(&message, &target) {
|
||||
return Ok(());
|
||||
}
|
||||
let guard = self.inner.lock().await;
|
||||
@@ -2535,6 +2539,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_poke_messages_are_dispatchable() {
|
||||
assert!(super::should_dispatch_text_message(
|
||||
"",
|
||||
&MessageTarget::Poke(42)
|
||||
));
|
||||
assert!(super::should_dispatch_text_message(
|
||||
" \t ",
|
||||
&MessageTarget::Poke(42)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_non_poke_messages_remain_suppressed() {
|
||||
assert!(!super::should_dispatch_text_message(
|
||||
"",
|
||||
&MessageTarget::Server
|
||||
));
|
||||
assert!(!super::should_dispatch_text_message(
|
||||
" ",
|
||||
&MessageTarget::Channel
|
||||
));
|
||||
assert!(!super::should_dispatch_text_message(
|
||||
"",
|
||||
&MessageTarget::Client(42)
|
||||
));
|
||||
assert!(super::should_dispatch_text_message(
|
||||
"hello",
|
||||
&MessageTarget::Channel
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_address_is_rejected() {
|
||||
let s = ChanoraSession::new();
|
||||
|
||||
@@ -49,6 +49,11 @@ terms.
|
||||
| `flutter` | 0.0.0 | sdk | yes |
|
||||
| `flutter_foreground_task` | 9.2.2 | hosted | yes |
|
||||
| `flutter_lints` | 6.0.0 | hosted | yes |
|
||||
| `flutter_local_notifications` | 22.0.0 | hosted | yes |
|
||||
| `flutter_local_notifications_linux` | 8.0.1 | hosted | yes |
|
||||
| `flutter_local_notifications_platform_interface` | 12.0.0 | hosted | yes |
|
||||
| `flutter_local_notifications_web` | 1.0.0 | hosted | yes |
|
||||
| `flutter_local_notifications_windows` | 3.1.0 | hosted | yes |
|
||||
| `flutter_localizations` | 0.0.0 | sdk | yes |
|
||||
| `flutter_rust_bridge` | 2.12.0 | hosted | yes |
|
||||
| `flutter_test` | 0.0.0 | sdk | yes |
|
||||
@@ -117,6 +122,7 @@ terms.
|
||||
| `string_scanner` | 1.4.1 | hosted | yes |
|
||||
| `term_glyph` | 1.2.2 | hosted | yes |
|
||||
| `test_api` | 0.7.11 | hosted | yes |
|
||||
| `timezone` | 0.11.0 | hosted | yes |
|
||||
| `typed_data` | 1.4.0 | hosted | yes |
|
||||
| `url_launcher` | 6.3.2 | hosted | yes |
|
||||
| `url_launcher_android` | 6.3.30 | hosted | yes |
|
||||
@@ -1869,6 +1875,166 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### flutter_local_notifications 22.0.0
|
||||
|
||||
```
|
||||
Copyright 2018 Michael Bui. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### flutter_local_notifications_linux 8.0.1
|
||||
|
||||
```
|
||||
Copyright 2018 Michael Bui. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### flutter_local_notifications_platform_interface 12.0.0
|
||||
|
||||
```
|
||||
Copyright 2020 Michael Bui. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### flutter_local_notifications_web 1.0.0
|
||||
|
||||
```
|
||||
Copyright 2020 Michael Bui. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### flutter_local_notifications_windows 3.1.0
|
||||
|
||||
```
|
||||
Copyright 2024 Michael Bui. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### flutter_localizations 0.0.0
|
||||
|
||||
```
|
||||
@@ -4673,6 +4839,33 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### timezone 0.11.0
|
||||
|
||||
```
|
||||
Copyright (c) 2014, timezone project authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### typed_data 1.4.0
|
||||
|
||||
```
|
||||
|
||||
@@ -291,7 +291,7 @@ those terms.
|
||||
| chanora_diagnostics | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
|
||||
| chanora_prefetch | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
|
||||
| chanora_protocol | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
|
||||
| chanora_resolver | 0.1.0 | `Apache License 2.0` | — |
|
||||
| chanora_resolver | 0.2.0-beta.1 | `Apache License 2.0` | — |
|
||||
| chanora_state | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
|
||||
| chanora_storage | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
|
||||
| alsa | 0.11.0 | `Apache License 2.0` | <https://github.com/diwic/alsa-rs> |
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# Poke Without Message Design
|
||||
|
||||
**Date:** 2026-06-09
|
||||
**Status:** Approved design for implementation
|
||||
**Scope:** Allow intentional TeamSpeak-compatible pokes without message text while preserving empty-message blocking for normal chat targets.
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Chanora should let a user poke another connected client without typing a message. A poke is an attention event, not an empty chat message. The UI should make that distinction explicit so the empty state is intentional, understandable, and safe from accidental spam.
|
||||
|
||||
The implementation target is narrow:
|
||||
|
||||
- Sending a poke with an empty message is allowed.
|
||||
- Sending an empty normal chat message remains blocked.
|
||||
- Incoming and historical empty pokes continue to render as poke events, not blank chat bubbles.
|
||||
- Existing poke notification behavior remains compatible with message and no-message pokes.
|
||||
|
||||
## 2. Research Summary
|
||||
|
||||
TeamSpeak-compatible poke behavior is command-like: the ServerQuery shape is `clientpoke clid={clientID} msg={text}`, backed by poke permissions such as `i_client_poke_power` and `i_client_needed_poke_power`. The product semantics are closer to an attention nudge than to a private text message.
|
||||
|
||||
Client behavior and community expectations point to two UX risks:
|
||||
|
||||
- The action can be useful without text because the sender often only wants attention.
|
||||
- The action can be abused as interruption spam, so the UI must keep the action deliberate and preserve existing receiver-side suppression and notification preferences.
|
||||
|
||||
The approved product direction is therefore to model no-message poke as a first-class attention event with optional text, rather than as an exception in the normal chat composer.
|
||||
|
||||
## 3. Recommended UX
|
||||
|
||||
Poke uses a poke-specific sending surface. The surface may reuse the current chat detail implementation internally, but the user-facing copy and validation must make the target type clear.
|
||||
|
||||
Required poke-target behavior:
|
||||
|
||||
| Element | Behavior |
|
||||
|---|---|
|
||||
| Header | Shows that the current surface is for poking the selected user. |
|
||||
| Text field | Optional message input. Placeholder should communicate that the message is optional. |
|
||||
| Primary action | Label is `Poke`, not `Send`. Enabled even when the trimmed message is empty. |
|
||||
| Empty send | Sends an intentional poke with `message: ''`. |
|
||||
| Non-empty send | Sends a poke with the typed message. |
|
||||
| History row | Empty poke renders as an attention event such as `Alice poked you`, never as a blank message. |
|
||||
|
||||
Required non-poke chat behavior:
|
||||
|
||||
| Target | Empty text behavior |
|
||||
|---|---|
|
||||
| Channel chat | Block send. |
|
||||
| Server chat | Block send. |
|
||||
| Private chat | Block send. |
|
||||
| Any future text-chat target | Block send unless it is explicitly modeled as a poke-like attention event. |
|
||||
|
||||
## 4. Architecture Boundaries
|
||||
|
||||
The change should stay inside the existing UI and bridge boundaries:
|
||||
|
||||
- Flutter owns presentation, composer validation, button enablement, localization copy, and widget tests.
|
||||
- Flutter Rust Bridge continues to pass typed `BridgeMessageTarget` and message text across the bridge.
|
||||
- Rust Core and Protocol continue to route `MessageTarget::Poke(client_id)` through the existing poke send path.
|
||||
- Protocol remains the only layer that knows how `tsclientlib` sends a TeamSpeak-compatible poke.
|
||||
|
||||
No new protocol concept is required. The existing bridge/protocol model already has `BridgeMessageTarget.poke` / `MessageTarget::Poke(u64)` and `client.poke(message)`. The key design change is target-aware composer validation in Flutter.
|
||||
|
||||
## 5. Implementation Design
|
||||
|
||||
The implementation should use a target-aware send policy.
|
||||
|
||||
For `BridgeMessageTarget.poke`:
|
||||
|
||||
- Do not reject an empty trimmed input.
|
||||
- Send the original or trimmed message according to the existing chat composer convention. If the current send path trims normal messages before sending, apply the same text normalization before passing the poke message.
|
||||
- Clear the composer after successful send, including empty-poke sends.
|
||||
- Preserve existing error and snackbar behavior for failed sends.
|
||||
|
||||
For all other `BridgeMessageTarget` variants:
|
||||
|
||||
- Keep the existing empty-trimmed-text guard.
|
||||
- Keep current button enablement and keyboard submit behavior unless those paths need target-aware adjustment to preserve the same empty-message block.
|
||||
|
||||
A simple policy helper is preferred over scattered conditionals. Example shape:
|
||||
|
||||
```dart
|
||||
bool canSendMessage({
|
||||
required BridgeMessageTarget target,
|
||||
required String text,
|
||||
}) {
|
||||
if (target is BridgeMessageTarget_Poke) {
|
||||
return true;
|
||||
}
|
||||
return text.trim().isNotEmpty;
|
||||
}
|
||||
```
|
||||
|
||||
The exact Dart type checks should follow the generated bridge type names used in the current codebase.
|
||||
|
||||
## 6. Notification And History Behavior
|
||||
|
||||
Existing no-message receiving behavior should remain the reference behavior:
|
||||
|
||||
- Incoming empty poke notification body falls back to text equivalent to `Alice pokes you`.
|
||||
- Incoming poke with message includes the message in the notification body.
|
||||
- Active-chat suppression and muted-sender preferences continue to apply.
|
||||
- Poke history rows distinguish poke events from normal chat rows.
|
||||
|
||||
The send-side change must not introduce a new blank message row shape. If the sender's local history records sent pokes, empty poke history should render as a poke action line with no empty bubble.
|
||||
|
||||
## 7. Abuse And Safety Rules
|
||||
|
||||
This slice does not add new anti-spam controls. It relies on existing TeamSpeak-compatible permissions, inbound poke strength/rate suppression, notification preferences, active-chat suppression, and muted sender handling.
|
||||
|
||||
The implementation must not weaken any existing receiver-side controls. If testing reveals that empty sent pokes bypass suppression, notification preferences, or history classification, that is a bug to fix in the same implementation pass.
|
||||
|
||||
Future follow-ups, not part of this slice:
|
||||
|
||||
- Per-sender or per-server outbound poke cooldown UI.
|
||||
- Receiver-side "never show poke dialog" equivalent beyond current notification preferences.
|
||||
- Dedicated poke inbox or grouped poke history.
|
||||
|
||||
## 8. Files Expected To Change
|
||||
|
||||
Expected implementation targets:
|
||||
|
||||
| File | Expected change |
|
||||
|---|---|
|
||||
| `apps/chanora_flutter/lib/widgets/chat_views.dart` | Make composer validation and action enablement target-aware for poke. Update poke placeholder/action copy if needed. |
|
||||
| `apps/chanora_flutter/test/widgets/chat_views_test.dart` | Add widget coverage for empty poke send and normal empty chat blocking. |
|
||||
|
||||
Optional targets if the implementation exposes missing copy or routing seams:
|
||||
|
||||
| File | Possible change |
|
||||
|---|---|
|
||||
| `apps/chanora_flutter/lib/main.dart` | Only if opening a poke target needs a clearer poke-specific title or route configuration. |
|
||||
| `apps/chanora_flutter/lib/l10n/*.arb` | Only if current copy cannot express optional poke messages without hard-coded strings. |
|
||||
| `apps/chanora_flutter/test/services/poke_notification_service_test.dart` | Only if send-side changes affect notification payload assumptions. |
|
||||
|
||||
The Rust protocol path should not need behavior changes unless tests prove that empty strings are blocked below Flutter.
|
||||
|
||||
## 9. Test Design
|
||||
|
||||
Required tests:
|
||||
|
||||
- Poke target shows an enabled primary `Poke` action when the text field is empty.
|
||||
- Tapping `Poke` on an empty poke target calls the send callback with `BridgeMessageTarget.poke` and an empty message.
|
||||
- Poke target still sends a typed message when text is present.
|
||||
- Normal channel/server/private chat targets keep blocking empty sends.
|
||||
- Empty poke history renders as a poke event line, not an empty text bubble.
|
||||
|
||||
Useful regression checks if already easy to target:
|
||||
|
||||
- Keyboard submit follows the same target-aware validation as the button.
|
||||
- Failed empty-poke send keeps existing error presentation.
|
||||
- Incoming empty poke notification tests still pass unchanged.
|
||||
|
||||
## 10. Validation
|
||||
|
||||
For the implementation branch, run focused Flutter verification first:
|
||||
|
||||
```text
|
||||
flutter test test/widgets/chat_views_test.dart
|
||||
flutter test test/services/poke_notification_service_test.dart
|
||||
flutter test test/services/poke_active_chat_test.dart
|
||||
flutter analyze
|
||||
```
|
||||
|
||||
If Rust or bridge files are touched, also run the matching Rust and bridge checks for the touched layer. Documentation-only changes require reading the affected spec and checking the diff; code tests are not required for this design commit.
|
||||
|
||||
## 11. Success Criteria
|
||||
|
||||
This design is implemented successfully when:
|
||||
|
||||
- A user can send a poke with no typed message.
|
||||
- Normal chat targets still reject empty sends.
|
||||
- The poke composer communicates that message text is optional.
|
||||
- Empty pokes are represented as poke events in history and notifications.
|
||||
- Existing poke notification preferences and suppression behavior remain intact.
|
||||
- Focused widget/service tests and `flutter analyze` pass, or any unrelated pre-existing failure is named with evidence.
|
||||
Reference in New Issue
Block a user