feat: отображение того что тебя пинговали в чате
This commit is contained in:
@@ -44,6 +44,9 @@ CachedChat? parseChatRow(
|
|||||||
final presence = _resolvePresence(type, otherId, presenceMap);
|
final presence = _resolvePresence(type, otherId, presenceMap);
|
||||||
final adminsOwner = _resolveAdmins(chat);
|
final adminsOwner = _resolveAdmins(chat);
|
||||||
final pinned = _resolvePinnedMessage(chat['pinnedMessage']);
|
final pinned = _resolvePinnedMessage(chat['pinnedMessage']);
|
||||||
|
final mentionId = int.tryParse(
|
||||||
|
chat['lastMentionMessageId']?.toString() ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
return CachedChat(
|
return CachedChat(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -71,6 +74,7 @@ CachedChat? parseChatRow(
|
|||||||
pinnedMsgText: pinned.text,
|
pinnedMsgText: pinned.text,
|
||||||
pinnedMsgTime: pinned.time,
|
pinnedMsgTime: pinned.time,
|
||||||
pinnedMsgIsPreview: pinned.isPreview,
|
pinnedMsgIsPreview: pinned.isPreview,
|
||||||
|
lastMentionMsgId: mentionId ?? existing[id]?.lastMentionMsgId,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e("Ошибка при парсинге чата: $e");
|
logger.e("Ошибка при парсинге чата: $e");
|
||||||
|
|||||||
@@ -37,6 +37,20 @@ Map<int, int> parseParticipants(dynamic raw) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Server message ids carry their timestamp in the high bits: the low 16 bits
|
||||||
|
/// are an intra-millisecond sequence number.
|
||||||
|
int messageIdToTime(int messageId) => messageId >> 16;
|
||||||
|
|
||||||
|
bool messageMentionsUser(Map<dynamic, dynamic> message, int userId) {
|
||||||
|
final elements = message['elements'];
|
||||||
|
if (elements is! List) return false;
|
||||||
|
for (final element in elements.whereType<Map>()) {
|
||||||
|
if (element['type']?.toString() != 'USER_MENTION') continue;
|
||||||
|
if (element['entityId'] == userId) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
class CachedChat {
|
class CachedChat {
|
||||||
final int id;
|
final int id;
|
||||||
final int accountId;
|
final int accountId;
|
||||||
@@ -65,6 +79,7 @@ class CachedChat {
|
|||||||
final String? pinnedMsgText;
|
final String? pinnedMsgText;
|
||||||
final int? pinnedMsgTime;
|
final int? pinnedMsgTime;
|
||||||
final bool pinnedMsgIsPreview;
|
final bool pinnedMsgIsPreview;
|
||||||
|
final int? lastMentionMsgId;
|
||||||
|
|
||||||
CachedChat({
|
CachedChat({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -93,6 +108,7 @@ class CachedChat {
|
|||||||
this.pinnedMsgText,
|
this.pinnedMsgText,
|
||||||
this.pinnedMsgTime,
|
this.pinnedMsgTime,
|
||||||
this.pinnedMsgIsPreview = false,
|
this.pinnedMsgIsPreview = false,
|
||||||
|
this.lastMentionMsgId,
|
||||||
}) : lastMsgTextOneLine = lastMsgText != null && lastMsgText.contains('\n')
|
}) : lastMsgTextOneLine = lastMsgText != null && lastMsgText.contains('\n')
|
||||||
? lastMsgText.replaceAll('\n', ' ')
|
? lastMsgText.replaceAll('\n', ' ')
|
||||||
: lastMsgText;
|
: lastMsgText;
|
||||||
@@ -137,6 +153,12 @@ class CachedChat {
|
|||||||
|
|
||||||
bool get isLastMsgDeleted => lastMsgText == ChatsModule.lastMsgPlaceholder;
|
bool get isLastMsgDeleted => lastMsgText == ChatsModule.lastMsgPlaceholder;
|
||||||
|
|
||||||
|
bool get hasUnreadMention {
|
||||||
|
final mentionId = lastMentionMsgId;
|
||||||
|
if (mentionId == null || mentionId <= 0) return false;
|
||||||
|
return messageIdToTime(mentionId) > (participants[accountId] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat(
|
factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat(
|
||||||
id: row['id'] as int,
|
id: row['id'] as int,
|
||||||
accountId: row['account_id'] as int,
|
accountId: row['account_id'] as int,
|
||||||
@@ -164,6 +186,7 @@ class CachedChat {
|
|||||||
pinnedMsgText: row['pinned_msg_text'] as String?,
|
pinnedMsgText: row['pinned_msg_text'] as String?,
|
||||||
pinnedMsgTime: row['pinned_msg_time'] as int?,
|
pinnedMsgTime: row['pinned_msg_time'] as int?,
|
||||||
pinnedMsgIsPreview: (row['pinned_msg_is_preview'] as int? ?? 0) == 1,
|
pinnedMsgIsPreview: (row['pinned_msg_is_preview'] as int? ?? 0) == 1,
|
||||||
|
lastMentionMsgId: row['last_mention_msg_id'] as int?,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Set<String> _decodeOptions(dynamic raw) {
|
static Set<String> _decodeOptions(dynamic raw) {
|
||||||
@@ -209,6 +232,7 @@ class CachedChat {
|
|||||||
'pinned_msg_text': pinnedMsgText,
|
'pinned_msg_text': pinnedMsgText,
|
||||||
'pinned_msg_time': pinnedMsgTime,
|
'pinned_msg_time': pinnedMsgTime,
|
||||||
'pinned_msg_is_preview': pinnedMsgIsPreview ? 1 : 0,
|
'pinned_msg_is_preview': pinnedMsgIsPreview ? 1 : 0,
|
||||||
|
'last_mention_msg_id': lastMentionMsgId,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const Object _keep = Object();
|
static const Object _keep = Object();
|
||||||
@@ -238,6 +262,7 @@ class CachedChat {
|
|||||||
Object? pinnedMsgText = _keep,
|
Object? pinnedMsgText = _keep,
|
||||||
Object? pinnedMsgTime = _keep,
|
Object? pinnedMsgTime = _keep,
|
||||||
bool? pinnedMsgIsPreview,
|
bool? pinnedMsgIsPreview,
|
||||||
|
Object? lastMentionMsgId = _keep,
|
||||||
}) {
|
}) {
|
||||||
return CachedChat(
|
return CachedChat(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -284,6 +309,9 @@ class CachedChat {
|
|||||||
? this.pinnedMsgTime
|
? this.pinnedMsgTime
|
||||||
: pinnedMsgTime as int?,
|
: pinnedMsgTime as int?,
|
||||||
pinnedMsgIsPreview: pinnedMsgIsPreview ?? this.pinnedMsgIsPreview,
|
pinnedMsgIsPreview: pinnedMsgIsPreview ?? this.pinnedMsgIsPreview,
|
||||||
|
lastMentionMsgId: identical(lastMentionMsgId, _keep)
|
||||||
|
? this.lastMentionMsgId
|
||||||
|
: lastMentionMsgId as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,8 +463,12 @@ class ChatsModule {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
row['unread_count'] = 0;
|
final cached = CachedChat.fromDbRow(row);
|
||||||
await AppDatabase.saveChats([row]);
|
final currentMark = cached.participants[accountId] ?? 0;
|
||||||
|
final participants = Map<int, int>.from(cached.participants)
|
||||||
|
..[accountId] = mark > currentMark ? mark : currentMark;
|
||||||
|
final updated = cached.copyWith(unreadCount: 0, participants: participants);
|
||||||
|
await AppDatabase.saveChats([updated.toDbRow()]);
|
||||||
_bump();
|
_bump();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,6 +850,13 @@ class ChatsModule {
|
|||||||
}
|
}
|
||||||
if (unread != null) newRow['unread_count'] = unread;
|
if (unread != null) newRow['unread_count'] = unread;
|
||||||
|
|
||||||
|
if (msgIdInt != null &&
|
||||||
|
status != 'REMOVED' &&
|
||||||
|
senderId != accountId &&
|
||||||
|
messageMentionsUser(msg, accountId)) {
|
||||||
|
newRow['last_mention_msg_id'] = msgIdInt;
|
||||||
|
}
|
||||||
|
|
||||||
final pinned = _extractPinnedMessage(msg);
|
final pinned = _extractPinnedMessage(msg);
|
||||||
if (pinned != null) {
|
if (pinned != null) {
|
||||||
newRow['pinned_msg_id'] = pinned.id;
|
newRow['pinned_msg_id'] = pinned.id;
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class AppDatabase {
|
|||||||
await _migrateLegacyDb(target);
|
await _migrateLegacyDb(target);
|
||||||
return openDatabase(
|
return openDatabase(
|
||||||
target,
|
target,
|
||||||
version: 19,
|
version: 20,
|
||||||
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
||||||
onCreate: (db, _) => _createTables(db),
|
onCreate: (db, _) => _createTables(db),
|
||||||
onUpgrade: (db, oldVersion, newVersion) async {
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
@@ -333,6 +333,14 @@ class AppDatabase {
|
|||||||
'INTEGER NOT NULL DEFAULT 0',
|
'INTEGER NOT NULL DEFAULT 0',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 20) {
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
db,
|
||||||
|
'chats_cache',
|
||||||
|
'last_mention_msg_id',
|
||||||
|
'INTEGER',
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -485,6 +493,7 @@ class AppDatabase {
|
|||||||
pinned_msg_text TEXT,
|
pinned_msg_text TEXT,
|
||||||
pinned_msg_time INTEGER,
|
pinned_msg_time INTEGER,
|
||||||
pinned_msg_is_preview INTEGER NOT NULL DEFAULT 0,
|
pinned_msg_is_preview INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_mention_msg_id INTEGER,
|
||||||
PRIMARY KEY (id, account_id)
|
PRIMARY KEY (id, account_id)
|
||||||
)
|
)
|
||||||
''';
|
''';
|
||||||
|
|||||||
@@ -1633,6 +1633,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
avatar ?? "",
|
avatar ?? "",
|
||||||
presenceUserId: secondId,
|
presenceUserId: secondId,
|
||||||
unreadCount: chat.unreadCount,
|
unreadCount: chat.unreadCount,
|
||||||
|
hasMention: chat.hasUnreadMention,
|
||||||
isMuted: chat.isMuted,
|
isMuted: chat.isMuted,
|
||||||
isVerified: isVerified,
|
isVerified: isVerified,
|
||||||
isPinned: isPinned,
|
isPinned: isPinned,
|
||||||
@@ -1691,6 +1692,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
? chat.iconUrl!
|
? chat.iconUrl!
|
||||||
: '',
|
: '',
|
||||||
unreadCount: chat.unreadCount,
|
unreadCount: chat.unreadCount,
|
||||||
|
hasMention: chat.hasUnreadMention,
|
||||||
isMuted: chat.isMuted,
|
isMuted: chat.isMuted,
|
||||||
isVerified: chat.isOfficial,
|
isVerified: chat.isOfficial,
|
||||||
isPinned: isPinned,
|
isPinned: isPinned,
|
||||||
@@ -2508,6 +2510,27 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _countBadge(ColorScheme cs, String label, {required bool muted}) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minWidth: 20),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: muted ? cs.surfaceContainerHighest : cs.primary,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: muted ? cs.outline : cs.onPrimary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildChatItem(
|
Widget _buildChatItem(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@@ -2517,6 +2540,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
int presenceUserId = 0,
|
int presenceUserId = 0,
|
||||||
bool isRead = false,
|
bool isRead = false,
|
||||||
int unreadCount = 0,
|
int unreadCount = 0,
|
||||||
|
bool hasMention = false,
|
||||||
bool isMuted = false,
|
bool isMuted = false,
|
||||||
bool isVerified = false,
|
bool isVerified = false,
|
||||||
bool isPinned = false,
|
bool isPinned = false,
|
||||||
@@ -2737,29 +2761,15 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
),
|
),
|
||||||
?statusIcon,
|
?statusIcon,
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
if (hasMention) ...[
|
||||||
|
_countBadge(cs, '@', muted: isMuted),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
if (unreadCount > 0)
|
if (unreadCount > 0)
|
||||||
Container(
|
_countBadge(
|
||||||
padding: const EdgeInsets.symmetric(
|
cs,
|
||||||
horizontal: 6,
|
unreadCount.toString(),
|
||||||
vertical: 2,
|
muted: isMuted,
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isMuted
|
|
||||||
? cs.surfaceContainerHighest
|
|
||||||
: cs.primary,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
unreadCount.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: isMuted
|
|
||||||
? cs.outline
|
|
||||||
: cs.onPrimary,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else if (isRead)
|
else if (isRead)
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:komet/backend/modules/chat_parsing.dart';
|
||||||
|
import 'package:komet/backend/modules/chats.dart';
|
||||||
|
|
||||||
|
const int _me = 4242;
|
||||||
|
const int _peer = 7331;
|
||||||
|
const int _chatId = -1000000000001;
|
||||||
|
|
||||||
|
const int _readMark = 1700000000000;
|
||||||
|
const int _mentionTime = 1700000005000;
|
||||||
|
const int _lastMsgTime = 1700000010000;
|
||||||
|
|
||||||
|
const int _mentionId = 111411200327680777;
|
||||||
|
const int _lastMsgId = 111411200655360012;
|
||||||
|
const int _oldMentionId = 111411196067840005;
|
||||||
|
|
||||||
|
CachedChat _chat({
|
||||||
|
int? mentionId,
|
||||||
|
int readMark = _readMark,
|
||||||
|
int unread = 2,
|
||||||
|
}) => CachedChat(
|
||||||
|
id: _chatId,
|
||||||
|
accountId: _me,
|
||||||
|
type: 'CHAT',
|
||||||
|
unreadCount: unread,
|
||||||
|
lastEventTime: _lastMsgTime,
|
||||||
|
cachedAt: 0,
|
||||||
|
dontDisturbUntil: 0,
|
||||||
|
isOnline: false,
|
||||||
|
seenTime: 0,
|
||||||
|
participants: {_me: readMark, _peer: _lastMsgTime},
|
||||||
|
lastMentionMsgId: mentionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('message id', () {
|
||||||
|
test('carries the timestamp in its high bits', () {
|
||||||
|
expect(messageIdToTime(_mentionId), _mentionTime);
|
||||||
|
expect(messageIdToTime(_lastMsgId), _lastMsgTime);
|
||||||
|
expect(messageIdToTime(_oldMentionId), _readMark - 60000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('hasUnreadMention', () {
|
||||||
|
test('is set when the mention is newer than my read mark', () {
|
||||||
|
expect(_chat(mentionId: _mentionId).hasUnreadMention, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stays off for a mention I have already read', () {
|
||||||
|
expect(_chat(mentionId: _oldMentionId).hasUnreadMention, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears once the read mark passes the mention', () {
|
||||||
|
final read = _chat(
|
||||||
|
mentionId: _mentionId,
|
||||||
|
readMark: _lastMsgTime,
|
||||||
|
unread: 0,
|
||||||
|
);
|
||||||
|
expect(read.hasUnreadMention, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is false without a mention id', () {
|
||||||
|
expect(_chat().hasUnreadMention, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('parseChatRow', () {
|
||||||
|
CachedChat parse(Map<String, dynamic> chat) => parseChatRow(
|
||||||
|
chat,
|
||||||
|
_me,
|
||||||
|
_me,
|
||||||
|
const {},
|
||||||
|
const {},
|
||||||
|
const {},
|
||||||
|
const {},
|
||||||
|
0,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
test('reads lastMentionMessageId from the server chat', () {
|
||||||
|
final chat = parse({
|
||||||
|
'id': _chatId,
|
||||||
|
'type': 'CHAT',
|
||||||
|
'title': 'test mention',
|
||||||
|
'newMessages': 2,
|
||||||
|
'lastEventTime': _lastMsgTime,
|
||||||
|
'participants': {'$_me': _readMark},
|
||||||
|
'lastMentionMessageId': '$_mentionId',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chat.lastMentionMsgId, _mentionId);
|
||||||
|
expect(chat.hasUnreadMention, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a chat without mentions keeps the badge off', () {
|
||||||
|
final chat = parse({
|
||||||
|
'id': _chatId,
|
||||||
|
'type': 'CHAT',
|
||||||
|
'title': 'test',
|
||||||
|
'lastEventTime': _lastMsgTime,
|
||||||
|
'participants': {'$_me': _readMark},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chat.lastMentionMsgId, isNull);
|
||||||
|
expect(chat.hasUnreadMention, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('messageMentionsUser', () {
|
||||||
|
test('matches a USER_MENTION addressed to me', () {
|
||||||
|
expect(
|
||||||
|
messageMentionsUser(const {
|
||||||
|
'elements': [
|
||||||
|
{'type': 'USER_MENTION', 'entityId': _me, 'length': 15},
|
||||||
|
],
|
||||||
|
}, _me),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores a mention of somebody else', () {
|
||||||
|
expect(
|
||||||
|
messageMentionsUser(const {
|
||||||
|
'elements': [
|
||||||
|
{'type': 'USER_MENTION', 'entityId': _peer, 'length': 15},
|
||||||
|
],
|
||||||
|
}, _me),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores messages without elements', () {
|
||||||
|
expect(messageMentionsUser(const {'text': 'privet'}, _me), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+16
-16
@@ -44,8 +44,8 @@ void main() {
|
|||||||
);
|
);
|
||||||
final query = mentionQueryAt(c.text, 3)!;
|
final query = mentionQueryAt(c.text, 3)!;
|
||||||
c.insertMention(
|
c.insertMention(
|
||||||
userId: 3079465,
|
userId: 555001,
|
||||||
name: 'Алексей Поляков',
|
name: 'Пётр Синицын',
|
||||||
start: query.start,
|
start: query.start,
|
||||||
end: query.end,
|
end: query.end,
|
||||||
);
|
);
|
||||||
@@ -55,9 +55,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final content = c.buildContent();
|
final content = c.buildContent();
|
||||||
expect(content.text, 'Алексей Поляков test');
|
expect(content.text, 'Пётр Синицын test');
|
||||||
expect(content.elements, [
|
expect(content.elements, [
|
||||||
{'type': 'USER_MENTION', 'from': 0, 'length': 15, 'entityId': 3079465},
|
{'type': 'USER_MENTION', 'from': 0, 'length': 12, 'entityId': 555001},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,21 +139,21 @@ void main() {
|
|||||||
test('a non-contact falls back to the ONEME name', () {
|
test('a non-contact falls back to the ONEME name', () {
|
||||||
final contact = info([
|
final contact = info([
|
||||||
{
|
{
|
||||||
'name': 'Алексей',
|
'name': 'Пётр',
|
||||||
'firstName': 'Алексей',
|
'firstName': 'Пётр',
|
||||||
'lastName': 'Поляков',
|
'lastName': 'Синицын',
|
||||||
'type': 'ONEME',
|
'type': 'ONEME',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(contact.fullName, 'Алексей Поляков');
|
expect(contact.fullName, 'Пётр Синицын');
|
||||||
expect(contact.isSavedContact, isFalse);
|
expect(contact.isSavedContact, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a custom name wins over the oneme one', () {
|
test('a custom name wins over the oneme one', () {
|
||||||
final contact = info([
|
final contact = info([
|
||||||
{'firstName': 'Лёша', 'lastName': 'сосед', 'type': 'CUSTOM'},
|
{'firstName': 'Лёша', 'lastName': 'сосед', 'type': 'CUSTOM'},
|
||||||
{'firstName': 'Алексей', 'lastName': 'Поляков', 'type': 'ONEME'},
|
{'firstName': 'Пётр', 'lastName': 'Синицын', 'type': 'ONEME'},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(contact.fullName, 'Лёша сосед');
|
expect(contact.fullName, 'Лёша сосед');
|
||||||
@@ -163,24 +163,24 @@ void main() {
|
|||||||
group('parseFormatElements', () {
|
group('parseFormatElements', () {
|
||||||
test('reads a server USER_MENTION without an explicit from', () {
|
test('reads a server USER_MENTION without an explicit from', () {
|
||||||
final ranges = parseFormatElements([
|
final ranges = parseFormatElements([
|
||||||
{'entityId': 3079465, 'type': 'USER_MENTION', 'length': 15},
|
{'entityId': 555001, 'type': 'USER_MENTION', 'length': 12},
|
||||||
]);
|
]);
|
||||||
expect(ranges.single.format, TextFormat.userMention);
|
expect(ranges.single.format, TextFormat.userMention);
|
||||||
expect(ranges.single.start, 0);
|
expect(ranges.single.start, 0);
|
||||||
expect(ranges.single.length, 15);
|
expect(ranges.single.length, 12);
|
||||||
expect(ranges.single.entityId, 3079465);
|
expect(ranges.single.entityId, 555001);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('segmentizeFormats carries the mention id onto its segment', () {
|
test('segmentizeFormats carries the mention id onto its segment', () {
|
||||||
final segments = segmentizeFormats('Алексей Поляков test', const [
|
final segments = segmentizeFormats('Пётр Синицын test', const [
|
||||||
FormatRange(
|
FormatRange(
|
||||||
format: TextFormat.userMention,
|
format: TextFormat.userMention,
|
||||||
start: 0,
|
start: 0,
|
||||||
length: 15,
|
length: 12,
|
||||||
entityId: 3079465,
|
entityId: 555001,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
expect(segments.first.mentionId, 3079465);
|
expect(segments.first.mentionId, 555001);
|
||||||
expect(segments.last.mentionId, isNull);
|
expect(segments.last.mentionId, isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ CachedMessage _message({
|
|||||||
'message': {
|
'message': {
|
||||||
'id': '9',
|
'id': '9',
|
||||||
'sender': _me,
|
'sender': _me,
|
||||||
'text': 'Алексей Поляков написал очень длинный ответ',
|
'text': 'Пётр Синицын написал очень длинный ответ',
|
||||||
'time': 0,
|
'time': 0,
|
||||||
'attaches': [],
|
'attaches': [],
|
||||||
},
|
},
|
||||||
@@ -68,14 +68,14 @@ Rect _rectOf(WidgetTester tester, Finder finder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUp(() => ContactCache.put(_peer, 'Алексей Поляков123'));
|
setUp(() => ContactCache.put(_peer, 'Пётр Синицын'));
|
||||||
|
|
||||||
testWidgets('a long sender name pushes the clock to the bubble edge', (
|
testWidgets('a long sender name pushes the clock to the bubble edge', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await _pumpBubble(tester, _message(text: 'нет'));
|
await _pumpBubble(tester, _message(text: 'нет'));
|
||||||
|
|
||||||
final header = _rectOf(tester, find.text('Алексей Поляков123'));
|
final header = _rectOf(tester, find.text('Пётр Синицын'));
|
||||||
final clock = _rectOf(tester, find.textContaining('05:46'));
|
final clock = _rectOf(tester, find.textContaining('05:46'));
|
||||||
final body = _rectOf(tester, find.text('нет'));
|
final body = _rectOf(tester, find.text('нет'));
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ void main() {
|
|||||||
) async {
|
) async {
|
||||||
await _pumpBubble(tester, _message(text: 'нет', withReply: true));
|
await _pumpBubble(tester, _message(text: 'нет', withReply: true));
|
||||||
|
|
||||||
final header = _rectOf(tester, find.text('Алексей Поляков123'));
|
final header = _rectOf(tester, find.text('Пётр Синицын'));
|
||||||
final label = _rectOf(tester, find.text('Вы'));
|
final label = _rectOf(tester, find.text('Вы'));
|
||||||
final quote = _rectOf(
|
final quote = _rectOf(
|
||||||
tester,
|
tester,
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ import 'package:komet/core/utils/text_entities.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('detectTextEntities', () {
|
group('detectTextEntities', () {
|
||||||
test('finds a phone and a card in one message', () {
|
test('finds a phone and a card in one message', () {
|
||||||
final found = detectTextEntities('+79231234567 тест 2200123456789019');
|
final found = detectTextEntities('+70001234567 тест 2200123456789019');
|
||||||
|
|
||||||
expect(found, hasLength(2));
|
expect(found, hasLength(2));
|
||||||
expect(found.first.kind, TextEntityKind.phone);
|
expect(found.first.kind, TextEntityKind.phone);
|
||||||
expect(found.first.value, '+79231234567');
|
expect(found.first.value, '+70001234567');
|
||||||
expect(found.last.kind, TextEntityKind.card);
|
expect(found.last.kind, TextEntityKind.card);
|
||||||
expect(found.last.value, '2200123456789019');
|
expect(found.last.value, '2200123456789019');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('finds a bare russian phone and a spaced card', () {
|
test('finds a bare russian phone and a spaced card', () {
|
||||||
final found = detectTextEntities('89231234567 и 2200 1234 5678 9019');
|
final found = detectTextEntities('80001234567 и 2200 1234 5678 9019');
|
||||||
expect(found.map((e) => e.kind), [
|
expect(found.map((e) => e.kind), [
|
||||||
TextEntityKind.phone,
|
TextEntityKind.phone,
|
||||||
TextEntityKind.card,
|
TextEntityKind.card,
|
||||||
]);
|
]);
|
||||||
expect(found.first.value, '+89231234567');
|
expect(found.first.value, '+80001234567');
|
||||||
expect(found.last.value, '2200123456789019');
|
expect(found.last.value, '2200123456789019');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ignores digits that are not a valid card', () {
|
test('ignores digits that are not a valid card', () {
|
||||||
expect(detectTextEntities('116984447620359334'), isEmpty);
|
expect(detectTextEntities('111411200327680777'), isEmpty);
|
||||||
expect(detectTextEntities('2200123456789018'), isEmpty);
|
expect(detectTextEntities('2200123456789018'), isEmpty);
|
||||||
expect(detectTextEntities('1234567890123456'), isEmpty);
|
expect(detectTextEntities('1234567890123456'), isEmpty);
|
||||||
});
|
});
|
||||||
@@ -34,23 +34,23 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('finds a nickname but not an email', () {
|
test('finds a nickname but not an email', () {
|
||||||
final found = detectTextEntities('привет @GroupGuardBot и mail@ya.ru');
|
final found = detectTextEntities('привет @ExampleBot и mail@ya.ru');
|
||||||
expect(found, hasLength(1));
|
expect(found, hasLength(1));
|
||||||
expect(found.single.kind, TextEntityKind.mention);
|
expect(found.single.kind, TextEntityKind.mention);
|
||||||
expect(found.single.value, 'GroupGuardBot');
|
expect(found.single.value, 'ExampleBot');
|
||||||
expect(found.single.start, 7);
|
expect(found.single.start, 7);
|
||||||
expect(found.single.end, 21);
|
expect(found.single.end, 18);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('finds a formatted profile phone', () {
|
test('finds a formatted profile phone', () {
|
||||||
final found = detectTextEntities('+7 (923) 123-45-67');
|
final found = detectTextEntities('+7 (000) 123-45-67');
|
||||||
expect(found, hasLength(1));
|
expect(found, hasLength(1));
|
||||||
expect(found.single.kind, TextEntityKind.phone);
|
expect(found.single.kind, TextEntityKind.phone);
|
||||||
expect(found.single.value, '+79231234567');
|
expect(found.single.value, '+70001234567');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('skips ranges that are already claimed', () {
|
test('skips ranges that are already claimed', () {
|
||||||
const text = 'https://max.ru/GroupGuardBot';
|
const text = 'https://max.ru/ExampleBot';
|
||||||
expect(
|
expect(
|
||||||
detectTextEntities(text, skip: [(start: 0, end: text.length)]),
|
detectTextEntities(text, skip: [(start: 0, end: text.length)]),
|
||||||
isEmpty,
|
isEmpty,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:komet/frontend/widgets/message_bubble.dart';
|
|||||||
import 'package:komet/frontend/widgets/text_entity_actions.dart';
|
import 'package:komet/frontend/widgets/text_entity_actions.dart';
|
||||||
import 'package:komet/l10n/app_localizations.dart';
|
import 'package:komet/l10n/app_localizations.dart';
|
||||||
|
|
||||||
const String _sample = '+79231234567 тест 2200123456789019 @GroupGuardBot';
|
const String _sample = '+70001234567 тест 2200123456789019 @ExampleBot';
|
||||||
|
|
||||||
CachedMessage _message(String text) => CachedMessage(
|
CachedMessage _message(String text) => CachedMessage(
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -67,9 +67,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final accent = ThemeData().colorScheme.primary;
|
final accent = ThemeData().colorScheme.primary;
|
||||||
final phone = _spanWithText(tester, '+79231234567');
|
final phone = _spanWithText(tester, '+70001234567');
|
||||||
final card = _spanWithText(tester, '2200123456789019');
|
final card = _spanWithText(tester, '2200123456789019');
|
||||||
final mention = _spanWithText(tester, '@GroupGuardBot');
|
final mention = _spanWithText(tester, '@ExampleBot');
|
||||||
final plain = _spanWithText(tester, ' тест ');
|
final plain = _spanWithText(tester, ' тест ');
|
||||||
|
|
||||||
expect(phone?.style?.color, accent);
|
expect(phone?.style?.color, accent);
|
||||||
@@ -90,12 +90,12 @@ void main() {
|
|||||||
accountId: 1,
|
accountId: 1,
|
||||||
chatId: 2,
|
chatId: 2,
|
||||||
senderId: 1,
|
senderId: 1,
|
||||||
text: '@GroupGuardBot test',
|
text: '@ExampleBot test',
|
||||||
time: DateTime(2026, 1, 1, 5, 46).millisecondsSinceEpoch,
|
time: DateTime(2026, 1, 1, 5, 46).millisecondsSinceEpoch,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
payload: const {
|
payload: const {
|
||||||
'elements': [
|
'elements': [
|
||||||
{'entityName': 'GroupGuardBot', 'type': 'USER_MENTION', 'length': 14},
|
{'entityName': 'ExampleBot', 'type': 'USER_MENTION', 'length': 11},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -105,7 +105,7 @@ void main() {
|
|||||||
MessageBubble(message: message, isMe: false, myId: 1, chatType: 'DIALOG'),
|
MessageBubble(message: message, isMe: false, myId: 1, chatType: 'DIALOG'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final mention = _spanWithText(tester, '@GroupGuardBot');
|
final mention = _spanWithText(tester, '@ExampleBot');
|
||||||
expect(mention?.style?.color, ThemeData().colorScheme.primary);
|
expect(mention?.style?.color, ThemeData().colorScheme.primary);
|
||||||
expect(mention?.recognizer, isA<TapGestureRecognizer>());
|
expect(mention?.recognizer, isA<TapGestureRecognizer>());
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
_spanWithText(tester, '+79231234567')?.recognizer,
|
_spanWithText(tester, '+70001234567')?.recognizer,
|
||||||
isA<TapGestureRecognizer>(),
|
isA<TapGestureRecognizer>(),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user