fix: delete forgotten files caused by mergetool
This commit is contained in:
parent
c6e0359522
commit
2b8782c699
File diff suppressed because it is too large
Load Diff
|
|
@ -1,329 +0,0 @@
|
||||||
/*
|
|
||||||
* Famedly Matrix SDK
|
|
||||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import '../matrix.dart';
|
|
||||||
import 'event.dart';
|
|
||||||
import 'event_status.dart';
|
|
||||||
import 'room.dart';
|
|
||||||
import 'utils/event_update.dart';
|
|
||||||
|
|
||||||
/// Represents the timeline of a room. The callback [onUpdate] will be triggered
|
|
||||||
/// automatically. The initial
|
|
||||||
/// event list will be retreived when created by the `room.getTimeline()` method.
|
|
||||||
class Timeline {
|
|
||||||
final Room room;
|
|
||||||
final List<Event> events;
|
|
||||||
|
|
||||||
/// Map of event ID to map of type to set of aggregated events
|
|
||||||
final Map<String, Map<String, Set<Event>>> aggregatedEvents = {};
|
|
||||||
|
|
||||||
final void Function()? onUpdate;
|
|
||||||
final void Function(int insertID)? onInsert;
|
|
||||||
|
|
||||||
StreamSubscription<EventUpdate>? sub;
|
|
||||||
StreamSubscription<SyncUpdate>? roomSub;
|
|
||||||
StreamSubscription<String>? sessionIdReceivedSub;
|
|
||||||
bool isRequestingHistory = false;
|
|
||||||
|
|
||||||
final Map<String, Event> _eventCache = {};
|
|
||||||
|
|
||||||
/// Searches for the event in this timeline. If not
|
|
||||||
/// found, requests from the server. Requested events
|
|
||||||
/// are cached.
|
|
||||||
Future<Event?> getEventById(String id) async {
|
|
||||||
for (final event in events) {
|
|
||||||
if (event.eventId == id) return event;
|
|
||||||
}
|
|
||||||
if (_eventCache.containsKey(id)) return _eventCache[id];
|
|
||||||
final requestedEvent = await room.getEventById(id);
|
|
||||||
if (requestedEvent == null) return null;
|
|
||||||
_eventCache[id] = requestedEvent;
|
|
||||||
return _eventCache[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
// When fetching history, we will collect them into the `_historyUpdates` set
|
|
||||||
// first, and then only process all events at once, once we have the full history.
|
|
||||||
// This ensures that the entire history fetching only triggers `onUpdate` only *once*,
|
|
||||||
// even if /sync's complete while history is being proccessed.
|
|
||||||
bool _collectHistoryUpdates = false;
|
|
||||||
|
|
||||||
bool get canRequestHistory {
|
|
||||||
if (events.isEmpty) return true;
|
|
||||||
return events.last.type != EventTypes.RoomCreate;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> requestHistory(
|
|
||||||
{int historyCount = Room.defaultHistoryCount}) async {
|
|
||||||
if (isRequestingHistory) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isRequestingHistory = true;
|
|
||||||
onUpdate?.call();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Look up for events in hive first
|
|
||||||
final eventsFromStore = await room.client.database?.getEventList(
|
|
||||||
room,
|
|
||||||
start: events.length,
|
|
||||||
limit: Room.defaultHistoryCount,
|
|
||||||
);
|
|
||||||
if (eventsFromStore != null && eventsFromStore.isNotEmpty) {
|
|
||||||
events.addAll(eventsFromStore);
|
|
||||||
} else {
|
|
||||||
Logs().v('No more events found in the store. Request from server...');
|
|
||||||
await room.requestHistory(
|
|
||||||
historyCount: historyCount,
|
|
||||||
onHistoryReceived: () {
|
|
||||||
_collectHistoryUpdates = true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
_collectHistoryUpdates = false;
|
|
||||||
isRequestingHistory = false;
|
|
||||||
onUpdate?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timeline(
|
|
||||||
{required this.room, List<Event>? events, this.onUpdate, this.onInsert})
|
|
||||||
: events = events ?? [] {
|
|
||||||
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
|
|
||||||
// If the timeline is limited we want to clear our events cache
|
|
||||||
roomSub = room.client.onSync.stream
|
|
||||||
.where((sync) => sync.rooms?.join?[room.id]?.timeline?.limited == true)
|
|
||||||
.listen((_) {
|
|
||||||
this.events.clear();
|
|
||||||
aggregatedEvents.clear();
|
|
||||||
});
|
|
||||||
sessionIdReceivedSub =
|
|
||||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
|
||||||
|
|
||||||
// we want to populate our aggregated events
|
|
||||||
for (final e in this.events) {
|
|
||||||
addAggregatedEvent(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Don't forget to call this before you dismiss this object!
|
|
||||||
void cancelSubscriptions() {
|
|
||||||
sub?.cancel();
|
|
||||||
roomSub?.cancel();
|
|
||||||
sessionIdReceivedSub?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sessionKeyReceived(String sessionId) async {
|
|
||||||
var decryptAtLeastOneEvent = false;
|
|
||||||
final decryptFn = () async {
|
|
||||||
if (!room.client.encryptionEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (var i = 0; i < events.length; i++) {
|
|
||||||
if (events[i].type == EventTypes.Encrypted &&
|
|
||||||
events[i].messageType == MessageTypes.BadEncrypted &&
|
|
||||||
events[i].content['session_id'] == sessionId) {
|
|
||||||
events[i] = await room.client.encryption
|
|
||||||
.decryptRoomEvent(room.id, events[i], store: true);
|
|
||||||
if (events[i].type != EventTypes.Encrypted) {
|
|
||||||
decryptAtLeastOneEvent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (room.client.database != null) {
|
|
||||||
await room.client.database.transaction(decryptFn);
|
|
||||||
} else {
|
|
||||||
await decryptFn();
|
|
||||||
}
|
|
||||||
if (decryptAtLeastOneEvent) onUpdate?.call();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request the keys for undecryptable events of this timeline
|
|
||||||
void requestKeys() {
|
|
||||||
for (final event in events) {
|
|
||||||
if (event.type == EventTypes.Encrypted &&
|
|
||||||
event.messageType == MessageTypes.BadEncrypted &&
|
|
||||||
event.content['can_request_session'] == true) {
|
|
||||||
try {
|
|
||||||
room.client.encryption.keyManager.maybeAutoRequest(room.id,
|
|
||||||
event.content['session_id'], event.content['sender_key']);
|
|
||||||
} catch (_) {
|
|
||||||
// dispose
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int _findEvent({String? event_id, String? unsigned_txid}) {
|
|
||||||
// we want to find any existing event where either the passed event_id or the passed unsigned_txid
|
|
||||||
// matches either the event_id or transaction_id of the existing event.
|
|
||||||
// For that we create two sets, searchNeedle, what we search, and searchHaystack, where we check if there is a match.
|
|
||||||
// Now, after having these two sets, if the intersect between them is non-empty, we know that we have at least one match in one pair,
|
|
||||||
// thus meaning we found our element.
|
|
||||||
final searchNeedle = <String>{};
|
|
||||||
if (event_id != null) {
|
|
||||||
searchNeedle.add(event_id);
|
|
||||||
}
|
|
||||||
if (unsigned_txid != null) {
|
|
||||||
searchNeedle.add(unsigned_txid);
|
|
||||||
}
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < events.length; i++) {
|
|
||||||
final searchHaystack = <String>{};
|
|
||||||
if (events[i].eventId != null) {
|
|
||||||
searchHaystack.add(events[i].eventId);
|
|
||||||
}
|
|
||||||
if (events[i].unsigned != null &&
|
|
||||||
events[i].unsigned['transaction_id'] != null) {
|
|
||||||
searchHaystack.add(events[i].unsigned['transaction_id']);
|
|
||||||
}
|
|
||||||
if (searchNeedle.intersection(searchHaystack).isNotEmpty) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeEventFromSet(Set<Event> eventSet, Event event) {
|
|
||||||
eventSet.removeWhere((e) =>
|
|
||||||
e.matchesEventOrTransactionId(event.eventId) ||
|
|
||||||
(event.unsigned != null &&
|
|
||||||
e.matchesEventOrTransactionId(event.unsigned['transaction_id'])));
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAggregatedEvent(Event event) {
|
|
||||||
// we want to add an event to the aggregation tree
|
|
||||||
if (event.relationshipType == null || event.relationshipEventId == null) {
|
|
||||||
return; // nothing to do
|
|
||||||
}
|
|
||||||
if (!aggregatedEvents.containsKey(event.relationshipEventId)) {
|
|
||||||
aggregatedEvents[event.relationshipEventId] = <String, Set<Event>>{};
|
|
||||||
}
|
|
||||||
final events = (aggregatedEvents[event.relationshipEventId] ??=
|
|
||||||
<String, Set<Event>>{})[event.relationshipType] ??= <Event>{};
|
|
||||||
// remove a potential old event
|
|
||||||
_removeEventFromSet(events, event);
|
|
||||||
// add the new one
|
|
||||||
events.add(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeAggregatedEvent(Event event) {
|
|
||||||
aggregatedEvents.remove(event.eventId);
|
|
||||||
if (event.unsigned != null) {
|
|
||||||
aggregatedEvents.remove(event.unsigned['transaction_id']);
|
|
||||||
}
|
|
||||||
for (final types in aggregatedEvents.values) {
|
|
||||||
for (final events in types.values) {
|
|
||||||
_removeEventFromSet(events, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEventUpdate(EventUpdate eventUpdate, {bool update = true}) {
|
|
||||||
try {
|
|
||||||
if (eventUpdate.roomID != room.id) return;
|
|
||||||
|
|
||||||
if (eventUpdate.type != EventUpdateType.timeline &&
|
|
||||||
eventUpdate.type != EventUpdateType.history) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final status = eventStatusFromInt(eventUpdate.content['status'] ??
|
|
||||||
(eventUpdate.content['unsigned'] is Map<String, dynamic>
|
|
||||||
? eventUpdate.content['unsigned'][messageSendingStatusKey]
|
|
||||||
: null) ??
|
|
||||||
EventStatus.synced.intValue);
|
|
||||||
// Redaction events are handled as modification for existing events.
|
|
||||||
if (eventUpdate.content['type'] == EventTypes.Redaction) {
|
|
||||||
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
|
||||||
if (eventId < events.length) {
|
|
||||||
removeAggregatedEvent(events[eventId]);
|
|
||||||
events[eventId].setRedactionEvent(Event.fromJson(
|
|
||||||
eventUpdate.content,
|
|
||||||
room,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if (status.isRemoved) {
|
|
||||||
final i = _findEvent(event_id: eventUpdate.content['event_id']);
|
|
||||||
if (i < events.length) {
|
|
||||||
removeAggregatedEvent(events[i]);
|
|
||||||
events.removeAt(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final i = _findEvent(
|
|
||||||
event_id: eventUpdate.content['event_id'],
|
|
||||||
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
|
||||||
? eventUpdate.content['unsigned']['transaction_id']
|
|
||||||
: null);
|
|
||||||
|
|
||||||
if (i < events.length) {
|
|
||||||
// if the old status is larger than the new one, we also want to preserve the old status
|
|
||||||
final oldStatus = events[i].status;
|
|
||||||
events[i] = Event.fromJson(
|
|
||||||
eventUpdate.content,
|
|
||||||
room,
|
|
||||||
);
|
|
||||||
// do we preserve the status? we should allow 0 -> -1 updates and status increases
|
|
||||||
if ((latestEventStatus(status, oldStatus) == oldStatus) &&
|
|
||||||
!(status.isError && oldStatus.isSending)) {
|
|
||||||
events[i].status = oldStatus;
|
|
||||||
}
|
|
||||||
addAggregatedEvent(events[i]);
|
|
||||||
} else {
|
|
||||||
final newEvent = Event.fromJson(
|
|
||||||
eventUpdate.content,
|
|
||||||
room,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (eventUpdate.type == EventUpdateType.history &&
|
|
||||||
events.indexWhere(
|
|
||||||
(e) => e.eventId == eventUpdate.content['event_id']) !=
|
|
||||||
-1) return;
|
|
||||||
if (eventUpdate.type == EventUpdateType.history) {
|
|
||||||
events.add(newEvent);
|
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
} else if (status.isError) {
|
|
||||||
events.insert(events.firstIndexWhereNotError, newEvent);
|
|
||||||
>>>>>>> 8fe85aca09b947ce7417672ce57ebfce80f3133c
|
|
||||||
} else {
|
|
||||||
events.insert(events.firstIndexWhereNotError, newEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
addAggregatedEvent(newEvent);
|
|
||||||
onInsert?.call(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (update && !_collectHistoryUpdates) {
|
|
||||||
onUpdate?.call();
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().w('Handle event update failed', e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on List<Event> {
|
|
||||||
int get firstIndexWhereNotError {
|
|
||||||
if (isEmpty) return 0;
|
|
||||||
final index = indexWhere((event) => !event.status.isError);
|
|
||||||
if (index == -1) return length;
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
/*
|
|
||||||
* Famedly Matrix SDK
|
|
||||||
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import '../matrix.dart';
|
|
||||||
|
|
||||||
import 'event.dart';
|
|
||||||
import 'room.dart';
|
|
||||||
|
|
||||||
/// Represents a Matrix User which may be a participant in a Matrix Room.
|
|
||||||
class User extends Event {
|
|
||||||
factory User(
|
|
||||||
String id, {
|
|
||||||
String? membership,
|
|
||||||
String? displayName,
|
|
||||||
String? avatarUrl,
|
|
||||||
Room? room,
|
|
||||||
}) {
|
|
||||||
return User.fromState(
|
|
||||||
stateKey: id,
|
|
||||||
content: {
|
|
||||||
if (membership != null) 'membership': membership,
|
|
||||||
if (displayName != null) 'displayname': displayName,
|
|
||||||
if (avatarUrl != null) 'avatar_url': avatarUrl,
|
|
||||||
},
|
|
||||||
typeKey: EventTypes.RoomMember,
|
|
||||||
roomId: room?.id,
|
|
||||||
room: room,
|
|
||||||
originServerTs: DateTime.now(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
User.fromState(
|
|
||||||
{dynamic prevContent,
|
|
||||||
required String stateKey,
|
|
||||||
dynamic content,
|
|
||||||
required String typeKey,
|
|
||||||
String? eventId,
|
|
||||||
String? roomId,
|
|
||||||
String? senderId,
|
|
||||||
required DateTime originServerTs,
|
|
||||||
dynamic unsigned,
|
|
||||||
Room? room})
|
|
||||||
: super(
|
|
||||||
stateKey: stateKey,
|
|
||||||
prevContent: prevContent,
|
|
||||||
content: content,
|
|
||||||
type: typeKey,
|
|
||||||
eventId: eventId,
|
|
||||||
roomId: roomId,
|
|
||||||
senderId: senderId,
|
|
||||||
originServerTs: originServerTs,
|
|
||||||
unsigned: unsigned,
|
|
||||||
room: room);
|
|
||||||
|
|
||||||
/// The full qualified Matrix ID in the format @username:server.abc.
|
|
||||||
String get id => stateKey;
|
|
||||||
|
|
||||||
/// The displayname of the user if the user has set one.
|
|
||||||
<<<<<<< HEAD
|
|
||||||
String? get displayName =>
|
|
||||||
=======
|
|
||||||
String get displayName =>
|
|
||||||
>>>>>>> 8fe85aca09b947ce7417672ce57ebfce80f3133c
|
|
||||||
content?.tryGet<String>('displayname') ??
|
|
||||||
prevContent?.tryGet<String>('displayname');
|
|
||||||
|
|
||||||
/// Returns the power level of this user.
|
|
||||||
int get powerLevel => room?.getPowerLevelByUserId(id) ?? 0;
|
|
||||||
|
|
||||||
/// The membership status of the user. One of:
|
|
||||||
/// join
|
|
||||||
/// invite
|
|
||||||
/// leave
|
|
||||||
/// ban
|
|
||||||
Membership get membership => Membership.values.firstWhere((e) {
|
|
||||||
if (content['membership'] != null) {
|
|
||||||
return e.toString() == 'Membership.' + content['membership'];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, orElse: () => Membership.join);
|
|
||||||
|
|
||||||
/// The avatar if the user has one.
|
|
||||||
Uri? get avatarUrl => content != null && content.containsKey('avatar_url')
|
|
||||||
? (content['avatar_url'] is String
|
|
||||||
? Uri.tryParse(content['avatar_url'])
|
|
||||||
: null)
|
|
||||||
: (prevContent != null && prevContent['avatar_url'] is String
|
|
||||||
? Uri.tryParse(prevContent['avatar_url'])
|
|
||||||
: null);
|
|
||||||
|
|
||||||
/// Returns the displayname or the local part of the Matrix ID if the user
|
|
||||||
/// has no displayname. If [formatLocalpart] is true, then the localpart will
|
|
||||||
/// be formatted in the way, that all "_" characters are becomming white spaces and
|
|
||||||
/// the first character of each word becomes uppercase.
|
|
||||||
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
|
|
||||||
/// if there is no other displayname available. If not then this will return "Unknown user".
|
|
||||||
String calcDisplayname({
|
|
||||||
bool? formatLocalpart,
|
|
||||||
bool? mxidLocalPartFallback,
|
|
||||||
}) {
|
|
||||||
formatLocalpart ??= room?.client?.formatLocalpart ?? true;
|
|
||||||
mxidLocalPartFallback ??= room?.client?.mxidLocalPartFallback ?? true;
|
|
||||||
final displayName = this.displayName;
|
|
||||||
if (displayName != null && displayName.isNotEmpty) {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
final stateKey = this.stateKey;
|
|
||||||
if (stateKey != null && mxidLocalPartFallback) {
|
|
||||||
if (!formatLocalpart) {
|
|
||||||
return stateKey.localpart ?? '';
|
|
||||||
}
|
|
||||||
final words = stateKey.localpart?.replaceAll('_', ' ').split(' ') ?? [];
|
|
||||||
for (var i = 0; i < words.length; i++) {
|
|
||||||
if (words[i].isNotEmpty) {
|
|
||||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return words.join(' ');
|
|
||||||
}
|
|
||||||
return 'Unknown user';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call the Matrix API to kick this user from this room.
|
|
||||||
Future<void> kick() => room.kick(id);
|
|
||||||
|
|
||||||
/// Call the Matrix API to ban this user from this room.
|
|
||||||
Future<void> ban() => room.ban(id);
|
|
||||||
|
|
||||||
/// Call the Matrix API to unban this banned user from this room.
|
|
||||||
Future<void> unban() => room.unban(id);
|
|
||||||
|
|
||||||
/// Call the Matrix API to change the power level of this user.
|
|
||||||
Future<void> setPower(int power) => room.setPower(id, power);
|
|
||||||
|
|
||||||
/// Returns an existing direct chat ID with this user or creates a new one.
|
|
||||||
/// Returns null on error.
|
|
||||||
Future<String?> startDirectChat() => room.client.startDirectChat(id);
|
|
||||||
|
|
||||||
/// The newest presence of this user if there is any and null if not.
|
|
||||||
Presence? get presence => room.client.presences[id];
|
|
||||||
|
|
||||||
/// Whether the client is able to ban/unban this user.
|
|
||||||
bool get canBan => room.canBan && powerLevel < room.ownPowerLevel;
|
|
||||||
|
|
||||||
/// Whether the client is able to kick this user.
|
|
||||||
bool get canKick =>
|
|
||||||
[Membership.join, Membership.invite].contains(membership) &&
|
|
||||||
room.canKick &&
|
|
||||||
powerLevel < room.ownPowerLevel;
|
|
||||||
|
|
||||||
/// Whether the client is allowed to change the power level of this user.
|
|
||||||
/// Please be aware that you can only set the power level to at least your own!
|
|
||||||
bool get canChangePowerLevel =>
|
|
||||||
room.canChangePowerLevel && powerLevel < room.ownPowerLevel;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(dynamic other) => (other is User &&
|
|
||||||
other.id == id &&
|
|
||||||
other.room == room &&
|
|
||||||
other.membership == membership);
|
|
||||||
|
|
||||||
/// Get the mention text to use in a plain text body to mention this specific user
|
|
||||||
/// in this specific room
|
|
||||||
String get mention {
|
|
||||||
// if the displayname has [ or ] or : we can't build our more fancy stuff, so fall back to the id
|
|
||||||
// [] is used for the delimitors
|
|
||||||
// If we allowed : we could get collissions with the mxid fallbacks
|
|
||||||
final displayName = this.displayName;
|
|
||||||
if (displayName == null ||
|
|
||||||
displayName.isEmpty ||
|
|
||||||
{'[', ']', ':'}.any(displayName.contains)) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
final identifier = '@' +
|
|
||||||
// if we have non-word characters we need to surround with []
|
|
||||||
(RegExp(r'^\w+$').hasMatch(displayName)
|
|
||||||
? displayName
|
|
||||||
: '[$displayName]');
|
|
||||||
|
|
||||||
// get all the users with the same display name
|
|
||||||
final allUsersWithSameDisplayname = room.getParticipants();
|
|
||||||
allUsersWithSameDisplayname.removeWhere((user) =>
|
|
||||||
user.id == id ||
|
|
||||||
(user.displayName?.isEmpty ?? true) ||
|
|
||||||
user.displayName != displayName);
|
|
||||||
if (allUsersWithSameDisplayname.isEmpty) {
|
|
||||||
return identifier;
|
|
||||||
}
|
|
||||||
// ok, we have multiple users with the same display name....time to calculate a hash
|
|
||||||
final hashes = allUsersWithSameDisplayname.map((u) => _hash(u.id));
|
|
||||||
final ourHash = _hash(id);
|
|
||||||
// hash collission...just return our own mxid again
|
|
||||||
if (hashes.contains(ourHash)) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
return '$identifier#$ourHash';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the mention fragments for this user.
|
|
||||||
Set<String> get mentionFragments {
|
|
||||||
final displayName = this.displayName;
|
|
||||||
if (displayName == null ||
|
|
||||||
displayName.isEmpty ||
|
|
||||||
{'[', ']', ':'}.any(displayName.contains)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
final identifier = '@' +
|
|
||||||
// if we have non-word characters we need to surround with []
|
|
||||||
(RegExp(r'^\w+$').hasMatch(displayName)
|
|
||||||
? displayName
|
|
||||||
: '[$displayName]');
|
|
||||||
|
|
||||||
final hash = _hash(id);
|
|
||||||
return {identifier, '$identifier#$hash'};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const _maximumHashLength = 10000;
|
|
||||||
String _hash(String s) =>
|
|
||||||
(s.codeUnits.fold<int>(0, (a, b) => a + b) % _maximumHashLength).toString();
|
|
||||||
=======
|
|
||||||
String _hash(String s) =>
|
|
||||||
(s.codeUnits.fold<int>(0, (a, b) => a + b) % 10000).toString();
|
|
||||||
>>>>>>> 8fe85aca09b947ce7417672ce57ebfce80f3133c
|
|
||||||
Loading…
Reference in New Issue