mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
Add tic tac toe DM game
This commit is contained in:
@@ -27,6 +27,7 @@ import '../utils/toast_logger.dart';
|
|||||||
import '../utils/key_comparison.dart';
|
import '../utils/key_comparison.dart';
|
||||||
import '../utils/voice_message_parser.dart';
|
import '../utils/voice_message_parser.dart';
|
||||||
import '../utils/image_message_parser.dart';
|
import '../utils/image_message_parser.dart';
|
||||||
|
import '../utils/tictactoe_message_parser.dart';
|
||||||
import '../providers/image_provider.dart' as ip;
|
import '../providers/image_provider.dart' as ip;
|
||||||
import '../services/image_codec_service.dart';
|
import '../services/image_codec_service.dart';
|
||||||
import '../services/image_preferences.dart';
|
import '../services/image_preferences.dart';
|
||||||
@@ -428,6 +429,54 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _startTicTacToeGame() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_destinationType !=
|
||||||
|
MessageDestinationPreferences.destinationTypeContact ||
|
||||||
|
_selectedRecipient == null) {
|
||||||
|
ToastLogger.warning(
|
||||||
|
context,
|
||||||
|
'Tic-Tac-Toe works only in direct messages. Choose a contact first.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
|
ToastLogger.error(context, 'Not connected to device');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
|
||||||
|
if (devicePublicKey == null || devicePublicKey.length < 6) {
|
||||||
|
ToastLogger.error(context, 'Device key unavailable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final gameId = List.generate(
|
||||||
|
8,
|
||||||
|
(_) => math.Random.secure().nextInt(16).toRadixString(16),
|
||||||
|
).join();
|
||||||
|
final starterKey6 = devicePublicKey
|
||||||
|
.sublist(0, 6)
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
final startMessage = TicTacToeMessageParser.encodeStart(
|
||||||
|
gameId: gameId,
|
||||||
|
starterKey6: starterKey6,
|
||||||
|
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _sendToRecipient(
|
||||||
|
startMessage,
|
||||||
|
connectionProvider,
|
||||||
|
messagesProvider,
|
||||||
|
contactsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Image sending ───────────────────────────────────────────────────────────
|
// ── Image sending ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Future<void> _pickAndSendImage({
|
Future<void> _pickAndSendImage({
|
||||||
@@ -1077,6 +1126,15 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
_pickAndSendImage(source: ImageSource.camera);
|
_pickAndSendImage(source: ImageSource.camera);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.grid_3x3),
|
||||||
|
title: const Text('Start Tic-Tac-Toe'),
|
||||||
|
subtitle: const Text('DM only'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_startTicTacToeGame();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
163
lib/utils/tictactoe_message_parser.dart
Normal file
163
lib/utils/tictactoe_message_parser.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
enum TicTacToeEventType { start, move }
|
||||||
|
|
||||||
|
class TicTacToeEvent {
|
||||||
|
final TicTacToeEventType type;
|
||||||
|
final String gameId;
|
||||||
|
final String playerKey6;
|
||||||
|
final int? cell;
|
||||||
|
final int timestampSec;
|
||||||
|
|
||||||
|
const TicTacToeEvent({
|
||||||
|
required this.type,
|
||||||
|
required this.gameId,
|
||||||
|
required this.playerKey6,
|
||||||
|
this.cell,
|
||||||
|
required this.timestampSec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TicTacToeMessageParser {
|
||||||
|
static const String _prefix = 'TTT1:';
|
||||||
|
|
||||||
|
static bool isTicTacToe(String text) => text.startsWith(_prefix);
|
||||||
|
|
||||||
|
static TicTacToeEvent? tryParse(String text) {
|
||||||
|
if (!isTicTacToe(text)) return null;
|
||||||
|
final body = text.substring(_prefix.length);
|
||||||
|
final parts = body.split(':');
|
||||||
|
if (parts.length < 4) return null;
|
||||||
|
|
||||||
|
final action = parts[0];
|
||||||
|
final gameId = parts[1].toLowerCase();
|
||||||
|
if (!RegExp(r'^[0-9a-f]{8}$').hasMatch(gameId)) return null;
|
||||||
|
|
||||||
|
if (action == 'S' && parts.length == 4) {
|
||||||
|
final starterKey6 = parts[2].toLowerCase();
|
||||||
|
final ts = int.tryParse(parts[3]);
|
||||||
|
if (!RegExp(r'^[0-9a-f]{12}$').hasMatch(starterKey6)) return null;
|
||||||
|
if (ts == null || ts <= 0) return null;
|
||||||
|
return TicTacToeEvent(
|
||||||
|
type: TicTacToeEventType.start,
|
||||||
|
gameId: gameId,
|
||||||
|
playerKey6: starterKey6,
|
||||||
|
timestampSec: ts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == 'M' && parts.length == 5) {
|
||||||
|
final cell = int.tryParse(parts[2]);
|
||||||
|
final playerKey6 = parts[3].toLowerCase();
|
||||||
|
final ts = int.tryParse(parts[4]);
|
||||||
|
if (cell == null || cell < 0 || cell > 8) return null;
|
||||||
|
if (!RegExp(r'^[0-9a-f]{12}$').hasMatch(playerKey6)) return null;
|
||||||
|
if (ts == null || ts <= 0) return null;
|
||||||
|
return TicTacToeEvent(
|
||||||
|
type: TicTacToeEventType.move,
|
||||||
|
gameId: gameId,
|
||||||
|
playerKey6: playerKey6,
|
||||||
|
cell: cell,
|
||||||
|
timestampSec: ts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String encodeStart({
|
||||||
|
required String gameId,
|
||||||
|
required String starterKey6,
|
||||||
|
required int timestampSec,
|
||||||
|
}) {
|
||||||
|
return '$_prefix'
|
||||||
|
'S:${gameId.toLowerCase()}:${starterKey6.toLowerCase()}:$timestampSec';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String encodeMove({
|
||||||
|
required String gameId,
|
||||||
|
required int cell,
|
||||||
|
required String playerKey6,
|
||||||
|
required int timestampSec,
|
||||||
|
}) {
|
||||||
|
return '$_prefix'
|
||||||
|
'M:${gameId.toLowerCase()}:$cell:${playerKey6.toLowerCase()}:$timestampSec';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TicTacToeGameState {
|
||||||
|
final String gameId;
|
||||||
|
final String xPlayerKey6;
|
||||||
|
final String oPlayerKey6;
|
||||||
|
final List<String?> board; // 'X' / 'O' / null
|
||||||
|
final String nextSymbol;
|
||||||
|
final String? winnerSymbol;
|
||||||
|
|
||||||
|
const TicTacToeGameState({
|
||||||
|
required this.gameId,
|
||||||
|
required this.xPlayerKey6,
|
||||||
|
required this.oPlayerKey6,
|
||||||
|
required this.board,
|
||||||
|
required this.nextSymbol,
|
||||||
|
this.winnerSymbol,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isDraw =>
|
||||||
|
winnerSymbol == null && board.every((cell) => cell != null);
|
||||||
|
bool get isFinished => winnerSymbol != null || isDraw;
|
||||||
|
}
|
||||||
|
|
||||||
|
TicTacToeGameState buildTicTacToeState({
|
||||||
|
required String gameId,
|
||||||
|
required String xPlayerKey6,
|
||||||
|
required String oPlayerKey6,
|
||||||
|
required List<TicTacToeEvent> events,
|
||||||
|
}) {
|
||||||
|
final board = List<String?>.filled(9, null);
|
||||||
|
var next = 'X';
|
||||||
|
String? winner;
|
||||||
|
|
||||||
|
final sorted = [...events]
|
||||||
|
..sort((a, b) => a.timestampSec.compareTo(b.timestampSec));
|
||||||
|
|
||||||
|
for (final event in sorted) {
|
||||||
|
if (event.type != TicTacToeEventType.move) continue;
|
||||||
|
if (winner != null) break;
|
||||||
|
|
||||||
|
final expectedKey = next == 'X' ? xPlayerKey6 : oPlayerKey6;
|
||||||
|
final cell = event.cell;
|
||||||
|
if (cell == null) continue;
|
||||||
|
if (event.playerKey6 != expectedKey) continue;
|
||||||
|
if (board[cell] != null) continue;
|
||||||
|
|
||||||
|
board[cell] = next;
|
||||||
|
winner = _computeWinner(board);
|
||||||
|
next = next == 'X' ? 'O' : 'X';
|
||||||
|
}
|
||||||
|
|
||||||
|
return TicTacToeGameState(
|
||||||
|
gameId: gameId,
|
||||||
|
xPlayerKey6: xPlayerKey6,
|
||||||
|
oPlayerKey6: oPlayerKey6,
|
||||||
|
board: board,
|
||||||
|
nextSymbol: next,
|
||||||
|
winnerSymbol: winner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _computeWinner(List<String?> board) {
|
||||||
|
const lines = <List<int>>[
|
||||||
|
[0, 1, 2],
|
||||||
|
[3, 4, 5],
|
||||||
|
[6, 7, 8],
|
||||||
|
[0, 3, 6],
|
||||||
|
[1, 4, 7],
|
||||||
|
[2, 5, 8],
|
||||||
|
[0, 4, 8],
|
||||||
|
[2, 4, 6],
|
||||||
|
];
|
||||||
|
for (final line in lines) {
|
||||||
|
final a = board[line[0]];
|
||||||
|
if (a == null) continue;
|
||||||
|
if (a == board[line[1]] && a == board[line[2]]) return a;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -22,10 +22,12 @@ import '../../utils/sar_message_parser.dart';
|
|||||||
import '../../utils/key_comparison.dart';
|
import '../../utils/key_comparison.dart';
|
||||||
import '../../utils/voice_message_parser.dart';
|
import '../../utils/voice_message_parser.dart';
|
||||||
import '../../utils/image_message_parser.dart';
|
import '../../utils/image_message_parser.dart';
|
||||||
|
import '../../utils/tictactoe_message_parser.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../../utils/message_extensions.dart';
|
import '../../utils/message_extensions.dart';
|
||||||
import 'voice_message_bubble.dart';
|
import 'voice_message_bubble.dart';
|
||||||
import 'image_message_bubble.dart';
|
import 'image_message_bubble.dart';
|
||||||
|
import 'tictactoe_message_bubble.dart';
|
||||||
import 'message_trace_sheet.dart';
|
import 'message_trace_sheet.dart';
|
||||||
|
|
||||||
/// Reusable message bubble widget that displays messages with various types:
|
/// Reusable message bubble widget that displays messages with various types:
|
||||||
@@ -2180,6 +2182,11 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
else if (ImageEnvelope.isEnvelope(message.text) &&
|
else if (ImageEnvelope.isEnvelope(message.text) &&
|
||||||
!widget.isCompact)
|
!widget.isCompact)
|
||||||
ImageMessageBubble(message: message, isSentByMe: isOwnMessage)
|
ImageMessageBubble(message: message, isSentByMe: isOwnMessage)
|
||||||
|
// Tic-Tac-Toe control message content
|
||||||
|
else if (message.isContactMessage &&
|
||||||
|
TicTacToeMessageParser.isTicTacToe(message.text) &&
|
||||||
|
!widget.isCompact)
|
||||||
|
TicTacToeMessageBubble(message: message, isSentByMe: isOwnMessage)
|
||||||
// Regular message content
|
// Regular message content
|
||||||
else if (!message.isDrawing || widget.isCompact)
|
else if (!message.isDrawing || widget.isCompact)
|
||||||
Text(message.text, style: Theme.of(context).textTheme.bodyMedium),
|
Text(message.text, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
|||||||
268
lib/widgets/messages/tictactoe_message_bubble.dart
Normal file
268
lib/widgets/messages/tictactoe_message_bubble.dart
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../models/contact.dart';
|
||||||
|
import '../../models/message.dart';
|
||||||
|
import '../../providers/connection_provider.dart';
|
||||||
|
import '../../providers/contacts_provider.dart';
|
||||||
|
import '../../providers/messages_provider.dart';
|
||||||
|
import '../../utils/tictactoe_message_parser.dart';
|
||||||
|
import '../../utils/toast_logger.dart';
|
||||||
|
|
||||||
|
class TicTacToeMessageBubble extends StatelessWidget {
|
||||||
|
final Message message;
|
||||||
|
final bool isSentByMe;
|
||||||
|
|
||||||
|
const TicTacToeMessageBubble({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.isSentByMe,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final event = TicTacToeMessageParser.tryParse(message.text);
|
||||||
|
if (event == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final conn = context.watch<ConnectionProvider>();
|
||||||
|
final contacts = context.read<ContactsProvider>();
|
||||||
|
final messages = context.watch<MessagesProvider>().messages;
|
||||||
|
final selfKey = conn.deviceInfo.publicKey;
|
||||||
|
if (selfKey == null || selfKey.length < 6) {
|
||||||
|
return const Text('Tic-Tac-Toe unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
final selfKey6 = _key6Hex(selfKey);
|
||||||
|
final opponent = _resolveOpponentContact(
|
||||||
|
message: message,
|
||||||
|
contactsProvider: contacts,
|
||||||
|
isSentByMe: isSentByMe,
|
||||||
|
);
|
||||||
|
if (opponent == null) {
|
||||||
|
return const Text('Tic-Tac-Toe: opponent unknown');
|
||||||
|
}
|
||||||
|
final opponentKey6 = _key6Hex(opponent.publicKey);
|
||||||
|
|
||||||
|
final gameEvents = <TicTacToeEvent>[];
|
||||||
|
TicTacToeEvent? start;
|
||||||
|
for (final m in messages) {
|
||||||
|
if (!m.isContactMessage) continue;
|
||||||
|
final parsed = TicTacToeMessageParser.tryParse(m.text);
|
||||||
|
if (parsed == null || parsed.gameId != event.gameId) continue;
|
||||||
|
if (!_isSameDmThread(
|
||||||
|
message: m,
|
||||||
|
selfKey: selfKey,
|
||||||
|
opponentKey6: opponentKey6,
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parsed.type == TicTacToeEventType.start) {
|
||||||
|
start ??= parsed;
|
||||||
|
} else {
|
||||||
|
gameEvents.add(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start ??= event.type == TicTacToeEventType.start ? event : null;
|
||||||
|
if (start == null) {
|
||||||
|
return const Text('Tic-Tac-Toe: waiting for start');
|
||||||
|
}
|
||||||
|
|
||||||
|
final xPlayer = start.playerKey6;
|
||||||
|
final oPlayer = xPlayer == selfKey6 ? opponentKey6 : selfKey6;
|
||||||
|
final state = buildTicTacToeState(
|
||||||
|
gameId: event.gameId,
|
||||||
|
xPlayerKey6: xPlayer,
|
||||||
|
oPlayerKey6: oPlayer,
|
||||||
|
events: gameEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
final mySymbol = selfKey6 == state.xPlayerKey6 ? 'X' : 'O';
|
||||||
|
final isMyTurn = !state.isFinished && state.nextSymbol == mySymbol;
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 230),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tic-Tac-Toe · Game ${state.gameId}',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_BoardGrid(
|
||||||
|
board: state.board,
|
||||||
|
enabled: isMyTurn,
|
||||||
|
onTapCell: (idx) => _onCellTap(
|
||||||
|
context: context,
|
||||||
|
idx: idx,
|
||||||
|
state: state,
|
||||||
|
selfKey6: selfKey6,
|
||||||
|
opponent: opponent,
|
||||||
|
connectionProvider: conn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
_statusText(state: state, mySymbol: mySymbol),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onCellTap({
|
||||||
|
required BuildContext context,
|
||||||
|
required int idx,
|
||||||
|
required TicTacToeGameState state,
|
||||||
|
required String selfKey6,
|
||||||
|
required Contact opponent,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
}) async {
|
||||||
|
if (idx < 0 || idx > 8 || state.board[idx] != null) return;
|
||||||
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
|
ToastLogger.error(context, 'Not connected to device');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
final text = TicTacToeMessageParser.encodeMove(
|
||||||
|
gameId: state.gameId,
|
||||||
|
cell: idx,
|
||||||
|
playerKey6: selfKey6,
|
||||||
|
timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
final messageId = '${DateTime.now().millisecondsSinceEpoch}_ttt_move';
|
||||||
|
final senderPublicKeyPrefix = connectionProvider.deviceInfo.publicKey!
|
||||||
|
.sublist(0, 6);
|
||||||
|
|
||||||
|
final sentMessage = Message(
|
||||||
|
id: messageId,
|
||||||
|
messageType: MessageType.contact,
|
||||||
|
senderPublicKeyPrefix: senderPublicKeyPrefix,
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
text: text,
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
deliveryStatus: MessageDeliveryStatus.sending,
|
||||||
|
recipientPublicKey: opponent.publicKey,
|
||||||
|
);
|
||||||
|
messagesProvider.addSentMessage(sentMessage);
|
||||||
|
|
||||||
|
final sent = await connectionProvider.sendTextMessage(
|
||||||
|
contactPublicKey: opponent.publicKey,
|
||||||
|
text: text,
|
||||||
|
messageId: messageId,
|
||||||
|
contact: opponent,
|
||||||
|
);
|
||||||
|
if (!sent) {
|
||||||
|
messagesProvider.markMessageFailed(messageId);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ToastLogger.error(context, 'Failed to send Tic-Tac-Toe move');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _statusText({
|
||||||
|
required TicTacToeGameState state,
|
||||||
|
required String mySymbol,
|
||||||
|
}) {
|
||||||
|
if (state.winnerSymbol != null) {
|
||||||
|
return state.winnerSymbol == mySymbol ? 'You won' : 'Opponent won';
|
||||||
|
}
|
||||||
|
if (state.isDraw) return 'Draw';
|
||||||
|
return state.nextSymbol == mySymbol ? 'Your turn' : 'Opponent turn';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _key6Hex(Uint8List key) => key
|
||||||
|
.sublist(0, math.min(6, key.length))
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
static Contact? _resolveOpponentContact({
|
||||||
|
required Message message,
|
||||||
|
required ContactsProvider contactsProvider,
|
||||||
|
required bool isSentByMe,
|
||||||
|
}) {
|
||||||
|
if (isSentByMe && message.recipientPublicKey != null) {
|
||||||
|
return contactsProvider.findContactByKey(message.recipientPublicKey!);
|
||||||
|
}
|
||||||
|
final sender = message.senderPublicKeyPrefix;
|
||||||
|
if (sender == null || sender.length < 6) return null;
|
||||||
|
return contactsProvider.findContactByPrefix(
|
||||||
|
Uint8List.fromList(sender.sublist(0, 6)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _isSameDmThread({
|
||||||
|
required Message message,
|
||||||
|
required Uint8List selfKey,
|
||||||
|
required String opponentKey6,
|
||||||
|
}) {
|
||||||
|
final isOwn = message.isSentMessage || message.isFromSelf(selfKey);
|
||||||
|
if (isOwn) {
|
||||||
|
final recipient = message.recipientPublicKey;
|
||||||
|
if (recipient == null || recipient.length < 6) return false;
|
||||||
|
return _key6Hex(recipient) == opponentKey6;
|
||||||
|
}
|
||||||
|
final sender = message.senderPublicKeyPrefix;
|
||||||
|
if (sender == null || sender.length < 6) return false;
|
||||||
|
return _key6Hex(sender) == opponentKey6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BoardGrid extends StatelessWidget {
|
||||||
|
final List<String?> board;
|
||||||
|
final bool enabled;
|
||||||
|
final ValueChanged<int> onTapCell;
|
||||||
|
|
||||||
|
const _BoardGrid({
|
||||||
|
required this.board,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onTapCell,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
child: GridView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
),
|
||||||
|
itemCount: 9,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final value = board[idx];
|
||||||
|
return InkWell(
|
||||||
|
onTap: enabled && value == null ? () => onTapCell(idx) : null,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value ?? '',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user