Merge branch 'main' of https://github.com/famedly/matrix-dart-sdk
This commit is contained in:
commit
3ac7f6153a
|
|
@ -119,10 +119,9 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||||
- uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa
|
- uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.flutter_version }}
|
sdk: ${{ env.dart_version }}
|
||||||
cache: true
|
|
||||||
- name: Ensure SDK compiles on web
|
- name: Ensure SDK compiles on web
|
||||||
run: |
|
run: |
|
||||||
pushd web_test
|
pushd web_test
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
flutter_version=3.35.4
|
|
||||||
dart_version=3.9.2
|
dart_version=3.9.2
|
||||||
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -1,3 +1,37 @@
|
||||||
|
## [4.0.0] 13th November 2025
|
||||||
|
|
||||||
|
Matrix Dart SDK 4.0.0 comes with support for polls, adds first bits towards OIDC and improved
|
||||||
|
support for spaces and threads.
|
||||||
|
This release also fixes a major performance leak while updating user device keys in the sync loop.
|
||||||
|
Especially for larger accounts this should improve the performance a lot.
|
||||||
|
v4.0.0 It comes with some breaking changes:
|
||||||
|
|
||||||
|
#### Migration guide
|
||||||
|
|
||||||
|
- `Client.checkHomeserver()` now returns a fourth value. You can just ignore it if you don't need auth_metadata.
|
||||||
|
- `RelationshipType.reply` has been removed in favor of `Event.inReplyToEventId()` where you can set if you want to ignore fallbacks or not. This makes it easier to differenciate fallback replies and replies inside of a thread.
|
||||||
|
|
||||||
|
#### All changes
|
||||||
|
- feat: (BREAKING) Discover OIDC auth metadata on Client.checkHomeserver() (Christian Kußowski)
|
||||||
|
- feat: Allow init with access token (Christian Kußowski)
|
||||||
|
- feat: Implement msc 3381 polls (krille-chan)
|
||||||
|
- feat: Use small versions of bullet point characters (Kelrap)
|
||||||
|
- fix: Correctly remove space child (Christian Kußowski)
|
||||||
|
- fix: Set join rules with knowk_restricted and multiple allow condition room ids (Christian Kußowski)
|
||||||
|
- refactor: (BREAKING) Replace Event.relationshipType and Event.relationshipEventId with Event.inReplyToEventId() for replies. (Christian Kußowski)
|
||||||
|
- refactor: Add option to always call auth metadata (Christian Kußowski)
|
||||||
|
- refactor: Escape HTML tags before markdown rendering (Christian Kußowski)
|
||||||
|
- refactor: Make direct chat getter type safe (Christian Kußowski)
|
||||||
|
- refactor: Simpler update user device keys (Christian Kußowski)
|
||||||
|
- chore: Cache auth metadata response in client (Christian Kußowski)
|
||||||
|
- chore: Remove flutter from CI (Christian Kußowski)
|
||||||
|
|
||||||
|
## [3.0.2] 24th October 2025
|
||||||
|
|
||||||
|
- chore: bump vodozemac version to v0.4.0 (Karthikeyan S)
|
||||||
|
- refactor: merge onGroupCallState and onGroupCallEvent into matrixRTCEventStream with proper types (Karthikeyan S)
|
||||||
|
- test: matrixRTCEventStream emitted events in a group call (Karthikeyan S)
|
||||||
|
|
||||||
## [3.0.1] 15th October 2025
|
## [3.0.1] 15th October 2025
|
||||||
- feat: Make display sending event configurable in Room.sendEvent() (Christian Kußowski)
|
- feat: Make display sending event configurable in Room.sendEvent() (Christian Kußowski)
|
||||||
- chore: tidy up call membership event (td)
|
- chore: tidy up call membership event (td)
|
||||||
|
|
|
||||||
|
|
@ -1188,6 +1188,20 @@ class FakeMatrixApi extends BaseClient {
|
||||||
'errcode': 'M_FORBIDDEN',
|
'errcode': 'M_FORBIDDEN',
|
||||||
'error': 'Blabla',
|
'error': 'Blabla',
|
||||||
},
|
},
|
||||||
|
'/client/v1/auth_metadata': (var req) => {
|
||||||
|
'authorization_endpoint':
|
||||||
|
'https://fakeserver.notexisting/oauth2/auth',
|
||||||
|
'code_challenge_methods_supported': ['S256'],
|
||||||
|
'grant_types_supported': ['authorization_code', 'refresh_token'],
|
||||||
|
'issuer': 'https://fakeserver.notexisting/',
|
||||||
|
'registration_endpoint':
|
||||||
|
'https://fakeserver.notexisting/oauth2/clients/register',
|
||||||
|
'response_modes_supported': ['query', 'fragment'],
|
||||||
|
'response_types_supported': ['code'],
|
||||||
|
'revocation_endpoint':
|
||||||
|
'https://fakeserver.notexisting/oauth2/revoke',
|
||||||
|
'token_endpoint': 'https://fakeserver.notexisting/oauth2/token',
|
||||||
|
},
|
||||||
'/media/v3/preview_url?url=https%3A%2F%2Fmatrix.org&ts=10': (var req) => {
|
'/media/v3/preview_url?url=https%3A%2F%2Fmatrix.org&ts=10': (var req) => {
|
||||||
'og:title': 'Matrix Blog Post',
|
'og:title': 'Matrix Blog Post',
|
||||||
'og:description': 'This is a really cool blog post from matrix.org',
|
'og:description': 'This is a really cool blog post from matrix.org',
|
||||||
|
|
@ -1338,7 +1352,7 @@ class FakeMatrixApi extends BaseClient {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/client/v3/account/whoami': (var req) =>
|
'/client/v3/account/whoami': (var req) =>
|
||||||
{'user_id': 'alice@example.com'},
|
{'user_id': 'alice@example.com', 'device_id': 'ABCDEFGH'},
|
||||||
'/client/v3/capabilities': (var req) => {
|
'/client/v3/capabilities': (var req) => {
|
||||||
'capabilities': {
|
'capabilities': {
|
||||||
'm.change_password': {'enabled': false},
|
'm.change_password': {'enabled': false},
|
||||||
|
|
@ -2679,6 +2693,10 @@ class FakeMatrixApi extends BaseClient {
|
||||||
(var req) => {'event_id': '1234'},
|
(var req) => {'event_id': '1234'},
|
||||||
'/client/v3/rooms/!1234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234':
|
'/client/v3/rooms/!1234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234':
|
||||||
(var req) => {'event_id': '1234'},
|
(var req) => {'event_id': '1234'},
|
||||||
|
'/client/v3/rooms/!696r7674%3Aexample.com/send/org.matrix.msc3381.poll.start/1234':
|
||||||
|
(var req) => {'event_id': '1234'},
|
||||||
|
'/client/v3/rooms/!696r7674%3Aexample.com/send/org.matrix.msc3381.poll.response/1234':
|
||||||
|
(var req) => {'event_id': '1234'},
|
||||||
'/client/v3/pushrules/global/room/!localpart%3Aserver.abc': (var req) =>
|
'/client/v3/pushrules/global/room/!localpart%3Aserver.abc': (var req) =>
|
||||||
{},
|
{},
|
||||||
'/client/v3/pushrules/global/override/.m.rule.master/enabled':
|
'/client/v3/pushrules/global/override/.m.rule.master/enabled':
|
||||||
|
|
@ -2717,8 +2735,6 @@ class FakeMatrixApi extends BaseClient {
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/v3/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/account_data/m.marked_unread':
|
'/client/v3/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/account_data/m.marked_unread':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/v3/user/%40test%3AfakeServer.notExisting/account_data/m.direct':
|
|
||||||
(var req) => {},
|
|
||||||
'/client/v3/user/%40othertest%3AfakeServer.notExisting/account_data/m.direct':
|
'/client/v3/user/%40othertest%3AfakeServer.notExisting/account_data/m.direct':
|
||||||
(var req) => {},
|
(var req) => {},
|
||||||
'/client/v3/profile/%40alice%3Aexample.com/displayname': (var reqI) => {},
|
'/client/v3/profile/%40alice%3Aexample.com/displayname': (var reqI) => {},
|
||||||
|
|
@ -2753,6 +2769,14 @@ class FakeMatrixApi extends BaseClient {
|
||||||
(var reqI) => {
|
(var reqI) => {
|
||||||
'event_id': '42',
|
'event_id': '42',
|
||||||
},
|
},
|
||||||
|
'/client/v3/rooms/!calls%3Aexample.com/state/com.famedly.call.member/%40test%3AfakeServer.notExisting':
|
||||||
|
(var reqI) => {
|
||||||
|
'event_id': 'call_member_42',
|
||||||
|
},
|
||||||
|
'/client/v3/rooms/!calls%3Aexample.com/state/com.famedly.call.member/%40remoteuser%3Aexample.com':
|
||||||
|
(var reqI) => {
|
||||||
|
'event_id': 'call_member_remote_42',
|
||||||
|
},
|
||||||
'/client/v3/directory/list/room/!localpart%3Aexample.com': (var req) =>
|
'/client/v3/directory/list/room/!localpart%3Aexample.com': (var req) =>
|
||||||
{},
|
{},
|
||||||
'/client/v3/room_keys/version/5': (var req) => {},
|
'/client/v3/room_keys/version/5': (var req) => {},
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,9 @@ export 'msc_extensions/msc_2835_uia_login/msc_2835_uia_login.dart';
|
||||||
export 'msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart';
|
export 'msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart';
|
||||||
export 'msc_extensions/extension_timeline_export/timeline_export.dart';
|
export 'msc_extensions/extension_timeline_export/timeline_export.dart';
|
||||||
export 'msc_extensions/msc_4140_delayed_events/api.dart';
|
export 'msc_extensions/msc_4140_delayed_events/api.dart';
|
||||||
|
export 'msc_extensions/msc_3381_polls/models/poll_event_content.dart';
|
||||||
|
export 'msc_extensions/msc_3381_polls/poll_event_extension.dart';
|
||||||
|
export 'msc_extensions/msc_3381_polls/poll_room_extension.dart';
|
||||||
|
|
||||||
export 'src/utils/web_worker/web_worker_stub.dart'
|
export 'src/utils/web_worker/web_worker_stub.dart'
|
||||||
if (dart.library.js_interop) 'src/utils/web_worker/web_worker.dart';
|
if (dart.library.js_interop) 'src/utils/web_worker/web_worker.dart';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Polls
|
||||||
|
|
||||||
|
Implementation of [MSC-3381](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3381-polls.md).
|
||||||
|
|
||||||
|
```Dart
|
||||||
|
|
||||||
|
// Start a poll:
|
||||||
|
final pollEventId = await room.startPoll(
|
||||||
|
question: 'What do you like more?',
|
||||||
|
kind: PollKind.undisclosed,
|
||||||
|
maxSelections: 2,
|
||||||
|
answers: [
|
||||||
|
PollAnswer(
|
||||||
|
id: 'pepsi', // You should use `Client.generateUniqueTransactionId()` here
|
||||||
|
mText: 'Pepsi,
|
||||||
|
),
|
||||||
|
PollAnswer(
|
||||||
|
id: 'coca',
|
||||||
|
mText: 'Coca Cola,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if an event is a poll (Do this before performing any other action):
|
||||||
|
final isPoll = event.type == PollEventContent.startType;
|
||||||
|
|
||||||
|
// Get the poll content
|
||||||
|
final pollEventContent = event.parsedPollEventContent;
|
||||||
|
|
||||||
|
// Check if poll has not ended yet (do this before answerPoll or endPoll):
|
||||||
|
final hasEnded = event.getPollHasBeenEnded(timeline);
|
||||||
|
|
||||||
|
// Responde to a poll:
|
||||||
|
final respondeId = await event.answerPoll(['pepsi', 'coca']);
|
||||||
|
|
||||||
|
// Get poll responses:
|
||||||
|
final responses = event.getPollResponses(timeline);
|
||||||
|
|
||||||
|
for(final userId in responses.keys) {
|
||||||
|
print('$userId voted for ${responses[userId]}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// End poll:
|
||||||
|
final endPollId = await event.endPoll();
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class PollEventContent {
|
||||||
|
final String mText;
|
||||||
|
final PollStartContent pollStartContent;
|
||||||
|
|
||||||
|
const PollEventContent({
|
||||||
|
required this.mText,
|
||||||
|
required this.pollStartContent,
|
||||||
|
});
|
||||||
|
static const String mTextJsonKey = 'org.matrix.msc1767.text';
|
||||||
|
static const String startType = 'org.matrix.msc3381.poll.start';
|
||||||
|
static const String responseType = 'org.matrix.msc3381.poll.response';
|
||||||
|
static const String endType = 'org.matrix.msc3381.poll.end';
|
||||||
|
|
||||||
|
factory PollEventContent.fromJson(Map<String, dynamic> json) =>
|
||||||
|
PollEventContent(
|
||||||
|
mText: json[mTextJsonKey],
|
||||||
|
pollStartContent: PollStartContent.fromJson(json[startType]),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
mTextJsonKey: mText,
|
||||||
|
startType: pollStartContent.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollStartContent {
|
||||||
|
final PollKind? kind;
|
||||||
|
final int maxSelections;
|
||||||
|
final PollQuestion question;
|
||||||
|
final List<PollAnswer> answers;
|
||||||
|
|
||||||
|
const PollStartContent({
|
||||||
|
this.kind,
|
||||||
|
required this.maxSelections,
|
||||||
|
required this.question,
|
||||||
|
required this.answers,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PollStartContent.fromJson(Map<String, dynamic> json) =>
|
||||||
|
PollStartContent(
|
||||||
|
kind: PollKind.values
|
||||||
|
.singleWhereOrNull((kind) => kind.name == json['kind']),
|
||||||
|
maxSelections: json['max_selections'],
|
||||||
|
question: PollQuestion.fromJson(json['question']),
|
||||||
|
answers: (json['answers'] as List)
|
||||||
|
.map((i) => PollAnswer.fromJson(i))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
if (kind != null) 'kind': kind?.name,
|
||||||
|
'max_selections': maxSelections,
|
||||||
|
'question': question.toJson(),
|
||||||
|
'answers': answers.map((i) => i.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollQuestion {
|
||||||
|
final String mText;
|
||||||
|
|
||||||
|
const PollQuestion({
|
||||||
|
required this.mText,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PollQuestion.fromJson(Map<String, dynamic> json) => PollQuestion(
|
||||||
|
mText: json[PollEventContent.mTextJsonKey] ?? json['body'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
PollEventContent.mTextJsonKey: mText,
|
||||||
|
// Compatible with older Element versions
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': mText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollAnswer {
|
||||||
|
final String id;
|
||||||
|
final String mText;
|
||||||
|
|
||||||
|
const PollAnswer({required this.id, required this.mText});
|
||||||
|
|
||||||
|
factory PollAnswer.fromJson(Map<String, Object?> json) => PollAnswer(
|
||||||
|
id: json['id'] as String,
|
||||||
|
mText: json[PollEventContent.mTextJsonKey] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object?> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
PollEventContent.mTextJsonKey: mText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PollKind {
|
||||||
|
disclosed('org.matrix.msc3381.poll.disclosed'),
|
||||||
|
undisclosed('org.matrix.msc3381.poll.undisclosed');
|
||||||
|
|
||||||
|
const PollKind(this.name);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
extension PollEventExtension on Event {
|
||||||
|
PollEventContent get parsedPollEventContent {
|
||||||
|
assert(type == PollEventContent.startType);
|
||||||
|
return PollEventContent.fromJson(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Map of user IDs to a Set of answer IDs.
|
||||||
|
Map<String, Set<String>> getPollResponses(Timeline timeline) {
|
||||||
|
assert(type == PollEventContent.startType);
|
||||||
|
final aggregatedEvents = timeline.aggregatedEvents[eventId]
|
||||||
|
?[RelationshipTypes.reference]
|
||||||
|
?.toList();
|
||||||
|
if (aggregatedEvents == null || aggregatedEvents.isEmpty) return {};
|
||||||
|
aggregatedEvents
|
||||||
|
.removeWhere((event) => event.type != PollEventContent.responseType);
|
||||||
|
|
||||||
|
final responses = <String, Event>{};
|
||||||
|
|
||||||
|
final endPollEvent = _getEndPollEvent(timeline);
|
||||||
|
|
||||||
|
for (final event in aggregatedEvents) {
|
||||||
|
// Ignore older responses if we already have a newer one:
|
||||||
|
final existingEvent = responses[event.senderId];
|
||||||
|
if (existingEvent != null &&
|
||||||
|
existingEvent.originServerTs.isAfter(event.originServerTs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Ignore all responses sent **after** the poll end event:
|
||||||
|
if (endPollEvent != null &&
|
||||||
|
event.originServerTs.isAfter(endPollEvent.originServerTs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
responses[event.senderId] = event;
|
||||||
|
}
|
||||||
|
return responses.map(
|
||||||
|
(userId, event) => MapEntry(
|
||||||
|
userId,
|
||||||
|
event.content
|
||||||
|
.tryGetMap<String, Object?>(PollEventContent.responseType)
|
||||||
|
?.tryGetList<String>('answers')
|
||||||
|
?.toSet() ??
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Event? _getEndPollEvent(Timeline timeline) {
|
||||||
|
assert(type == PollEventContent.startType);
|
||||||
|
final aggregatedEvents =
|
||||||
|
timeline.aggregatedEvents[eventId]?[RelationshipTypes.reference];
|
||||||
|
if (aggregatedEvents == null || aggregatedEvents.isEmpty) return null;
|
||||||
|
|
||||||
|
final redactPowerLevel = (room
|
||||||
|
.getState(EventTypes.RoomPowerLevels)
|
||||||
|
?.content
|
||||||
|
.tryGet<int>('redact') ??
|
||||||
|
50);
|
||||||
|
|
||||||
|
return aggregatedEvents.firstWhereOrNull(
|
||||||
|
(event) {
|
||||||
|
if (event.content
|
||||||
|
.tryGetMap<String, Object?>(PollEventContent.endType) ==
|
||||||
|
null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a m.poll.end event is received from someone other than the poll
|
||||||
|
//creator or user with permission to redact other's messages in the
|
||||||
|
//room, the event must be ignored by clients due to being invalid.
|
||||||
|
if (event.senderId == senderId ||
|
||||||
|
event.senderFromMemoryOrFallback.powerLevel >= redactPowerLevel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Logs().w(
|
||||||
|
'Ignore poll end event form user without permission ${event.senderId}',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getPollHasBeenEnded(Timeline timeline) =>
|
||||||
|
_getEndPollEvent(timeline) != null;
|
||||||
|
|
||||||
|
Future<String?> answerPoll(
|
||||||
|
List<String> answerIds, {
|
||||||
|
String? txid,
|
||||||
|
}) {
|
||||||
|
if (type != PollEventContent.startType) {
|
||||||
|
throw Exception('Event is not a poll.');
|
||||||
|
}
|
||||||
|
if (answerIds.length >
|
||||||
|
parsedPollEventContent.pollStartContent.maxSelections) {
|
||||||
|
throw Exception('Selected more answers than allowed in this poll.');
|
||||||
|
}
|
||||||
|
return room.sendEvent(
|
||||||
|
{
|
||||||
|
'm.relates_to': {
|
||||||
|
'rel_type': RelationshipTypes.reference,
|
||||||
|
'event_id': eventId,
|
||||||
|
},
|
||||||
|
PollEventContent.responseType: {'answers': answerIds},
|
||||||
|
},
|
||||||
|
type: PollEventContent.responseType,
|
||||||
|
txid: txid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> endPoll({String? txid}) {
|
||||||
|
if (type != PollEventContent.startType) {
|
||||||
|
throw Exception('Event is not a poll.');
|
||||||
|
}
|
||||||
|
if (senderId != room.client.userID) {
|
||||||
|
throw Exception('You can not end a poll created by someone else.');
|
||||||
|
}
|
||||||
|
return room.sendEvent(
|
||||||
|
{
|
||||||
|
'm.relates_to': {
|
||||||
|
'rel_type': RelationshipTypes.reference,
|
||||||
|
'event_id': eventId,
|
||||||
|
},
|
||||||
|
PollEventContent.endType: {},
|
||||||
|
},
|
||||||
|
type: PollEventContent.endType,
|
||||||
|
txid: txid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
extension PollRoomExtension on Room {
|
||||||
|
Future<String?> startPoll({
|
||||||
|
required String question,
|
||||||
|
required List<PollAnswer> answers,
|
||||||
|
String? body,
|
||||||
|
PollKind kind = PollKind.undisclosed,
|
||||||
|
int maxSelections = 1,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
|
if (answers.length > 20) {
|
||||||
|
throw Exception('Client must not set more than 20 answers in a poll');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body == null) {
|
||||||
|
body = question;
|
||||||
|
for (var i = 0; i < answers.length; i++) {
|
||||||
|
body = '$body\n$i. ${answers[i].mText}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final newPollEvent = PollEventContent(
|
||||||
|
mText: body!,
|
||||||
|
pollStartContent: PollStartContent(
|
||||||
|
kind: kind,
|
||||||
|
maxSelections: maxSelections,
|
||||||
|
question: PollQuestion(mText: question),
|
||||||
|
answers: answers,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return sendEvent(
|
||||||
|
newPollEvent.toJson(),
|
||||||
|
type: PollEventContent.startType,
|
||||||
|
txid: txid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -133,7 +133,7 @@ class Client extends MatrixApi {
|
||||||
@override
|
@override
|
||||||
set homeserver(Uri? homeserver) {
|
set homeserver(Uri? homeserver) {
|
||||||
if (this.homeserver != null && homeserver?.host != this.homeserver?.host) {
|
if (this.homeserver != null && homeserver?.host != this.homeserver?.host) {
|
||||||
_wellKnown = null;
|
_wellKnown = _getAuthMetadataResponseCache = null;
|
||||||
}
|
}
|
||||||
super.homeserver = homeserver;
|
super.homeserver = homeserver;
|
||||||
}
|
}
|
||||||
|
|
@ -440,8 +440,13 @@ class Client extends MatrixApi {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> get directChats =>
|
Map<String, List<String>> get directChats =>
|
||||||
_accountData['m.direct']?.content ?? {};
|
(_accountData['m.direct']?.content ?? {}).map(
|
||||||
|
(userId, list) => MapEntry(
|
||||||
|
userId,
|
||||||
|
(list is! List) ? [] : list.whereType<String>().toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/// Returns the first room ID from the store (the room with the latest event)
|
/// Returns the first room ID from the store (the room with the latest event)
|
||||||
/// which is a private chat with the user [userId].
|
/// which is a private chat with the user [userId].
|
||||||
|
|
@ -516,9 +521,17 @@ class Client extends MatrixApi {
|
||||||
DiscoveryInformation?,
|
DiscoveryInformation?,
|
||||||
GetVersionsResponse versions,
|
GetVersionsResponse versions,
|
||||||
List<LoginFlow>,
|
List<LoginFlow>,
|
||||||
|
GetAuthMetadataResponse? authMetadata,
|
||||||
)> checkHomeserver(
|
)> checkHomeserver(
|
||||||
Uri homeserverUrl, {
|
Uri homeserverUrl, {
|
||||||
bool checkWellKnown = true,
|
bool checkWellKnown = true,
|
||||||
|
|
||||||
|
/// Weither this method should also call `/auth_metadata` to fetch
|
||||||
|
/// Matrix native OIDC information. Defaults to if the `/versions` endpoint
|
||||||
|
/// returns version v1.15 or higher. Set to `true` to always call the
|
||||||
|
/// endpoint if your homeserver supports the endpoint while not fully
|
||||||
|
/// supporting version v1.15 yet.
|
||||||
|
bool? fetchAuthMetadata,
|
||||||
Set<String>? overrideSupportedVersions,
|
Set<String>? overrideSupportedVersions,
|
||||||
}) async {
|
}) async {
|
||||||
final supportedVersions =
|
final supportedVersions =
|
||||||
|
|
@ -555,7 +568,21 @@ class Client extends MatrixApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (wellKnown, versions, loginTypes);
|
fetchAuthMetadata ??= versions.versions.any(
|
||||||
|
(v) => isVersionGreaterThanOrEqualTo(v, 'v1.15'),
|
||||||
|
);
|
||||||
|
GetAuthMetadataResponse? authMetadata;
|
||||||
|
if (fetchAuthMetadata) {
|
||||||
|
try {
|
||||||
|
authMetadata = await getAuthMetadata();
|
||||||
|
} on MatrixException catch (e, s) {
|
||||||
|
if (e.error != MatrixError.M_UNRECOGNIZED) {
|
||||||
|
Logs().w('Unable to discover OIDC auth metadata.', e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (wellKnown, versions, loginTypes, authMetadata);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
homeserver = null;
|
homeserver = null;
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|
@ -720,6 +747,12 @@ class Client extends MatrixApi {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GetAuthMetadataResponse? _getAuthMetadataResponseCache;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GetAuthMetadataResponse> getAuthMetadata() async =>
|
||||||
|
_getAuthMetadataResponseCache ??= await super.getAuthMetadata();
|
||||||
|
|
||||||
/// Sends a logout command to the homeserver and clears all local data,
|
/// Sends a logout command to the homeserver and clears all local data,
|
||||||
/// including all persistent data from the store.
|
/// including all persistent data from the store.
|
||||||
@override
|
@override
|
||||||
|
|
@ -1961,9 +1994,9 @@ class Client extends MatrixApi {
|
||||||
///
|
///
|
||||||
/// Sends [LoginState.loggedIn] to [onLoginStateChanged].
|
/// Sends [LoginState.loggedIn] to [onLoginStateChanged].
|
||||||
///
|
///
|
||||||
/// If one of [newToken], [newUserID], [newDeviceID], [newDeviceName] is set then
|
/// If one of [newToken] is set, but one of [newUserID], [newDeviceID] is
|
||||||
/// all of them must be set! If you don't set them, this method will try to
|
/// null, then this method calls `/whoami` to fetch user ID and device ID
|
||||||
/// get them from the database.
|
/// and rethrows if this request fails.
|
||||||
///
|
///
|
||||||
/// Set [waitForFirstSync] and [waitUntilLoadCompletedLoaded] to false to speed this
|
/// Set [waitForFirstSync] and [waitUntilLoadCompletedLoaded] to false to speed this
|
||||||
/// up. You can then wait for `roomsLoading`, `_accountDataLoading` and
|
/// up. You can then wait for `roomsLoading`, `_accountDataLoading` and
|
||||||
|
|
@ -1987,16 +2020,9 @@ class Client extends MatrixApi {
|
||||||
/// To track what actually happens you can set a callback here.
|
/// To track what actually happens you can set a callback here.
|
||||||
void Function(InitState)? onInitStateChanged,
|
void Function(InitState)? onInitStateChanged,
|
||||||
}) async {
|
}) async {
|
||||||
if ((newToken != null ||
|
if (newToken != null && homeserver == null && newHomeserver == null) {
|
||||||
newUserID != null ||
|
|
||||||
newDeviceID != null ||
|
|
||||||
newDeviceName != null) &&
|
|
||||||
(newToken == null ||
|
|
||||||
newUserID == null ||
|
|
||||||
newDeviceID == null ||
|
|
||||||
newDeviceName == null)) {
|
|
||||||
throw ClientInitPreconditionError(
|
throw ClientInitPreconditionError(
|
||||||
'If one of [newToken, newUserID, newDeviceID, newDeviceName] is set then all of them must be set!',
|
'init() can not be performed with an access token when no homeserver was specified.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2088,6 +2114,12 @@ class Client extends MatrixApi {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (accessToken != null && (userID == null || deviceID == null)) {
|
||||||
|
final userInfo = await getTokenOwner();
|
||||||
|
_userID = userID = userInfo.userId;
|
||||||
|
_deviceID = userInfo.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
if (accessToken == null || homeserver == null || userID == null) {
|
if (accessToken == null || homeserver == null || userID == null) {
|
||||||
if (legacyDatabaseBuilder != null) {
|
if (legacyDatabaseBuilder != null) {
|
||||||
await _migrateFromLegacyDatabase(
|
await _migrateFromLegacyDatabase(
|
||||||
|
|
@ -2236,8 +2268,8 @@ class Client extends MatrixApi {
|
||||||
await dispose();
|
await dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_id = accessToken = _syncFilterId =
|
_id = accessToken = _syncFilterId = homeserver =
|
||||||
homeserver = _userID = _deviceID = _deviceName = _prevBatch = null;
|
_userID = _deviceID = _deviceName = _prevBatch = _trackedUserIds = null;
|
||||||
_rooms = [];
|
_rooms = [];
|
||||||
_eventsPendingDecryption.clear();
|
_eventsPendingDecryption.clear();
|
||||||
await encryption?.dispose();
|
await encryption?.dispose();
|
||||||
|
|
@ -2548,6 +2580,7 @@ class Client extends MatrixApi {
|
||||||
for (final userId in deviceLists.left ?? []) {
|
for (final userId in deviceLists.left ?? []) {
|
||||||
if (_userDeviceKeys.containsKey(userId)) {
|
if (_userDeviceKeys.containsKey(userId)) {
|
||||||
_userDeviceKeys.remove(userId);
|
_userDeviceKeys.remove(userId);
|
||||||
|
_trackedUserIds?.remove(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2817,16 +2850,26 @@ class Client extends MatrixApi {
|
||||||
final callEvents = <Event>[];
|
final callEvents = <Event>[];
|
||||||
|
|
||||||
for (var event in events) {
|
for (var event in events) {
|
||||||
// The client must ignore any new m.room.encryption event to prevent
|
if (event.type == EventTypes.Encryption) {
|
||||||
// man-in-the-middle attacks!
|
// The client must ignore any new m.room.encryption event to prevent
|
||||||
if ((event.type == EventTypes.Encryption &&
|
// man-in-the-middle attacks!
|
||||||
room.encrypted &&
|
if ((room.encrypted &&
|
||||||
event.content.tryGet<String>('algorithm') !=
|
event.content.tryGet<String>('algorithm') !=
|
||||||
room
|
room
|
||||||
.getState(EventTypes.Encryption)
|
.getState(EventTypes.Encryption)
|
||||||
?.content
|
?.content
|
||||||
.tryGet<String>('algorithm'))) {
|
.tryGet<String>('algorithm'))) {
|
||||||
continue;
|
Logs().wtf(
|
||||||
|
'Received an `m.room.encryption` event in a room, where encryption is already enabled! This event must be ignored as it could be an attack!',
|
||||||
|
jsonEncode(event.toJson()),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Encryption has been enabled in a room -> Reset tracked user IDs so
|
||||||
|
// sync they can be calculated again.
|
||||||
|
Logs().i('End to end encryption enabled in', room.id);
|
||||||
|
_trackedUserIds = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event is MatrixEvent &&
|
if (event is MatrixEvent &&
|
||||||
|
|
@ -3060,10 +3103,20 @@ class Client extends MatrixApi {
|
||||||
final event = Event.fromMatrixEvent(eventUpdate, room);
|
final event = Event.fromMatrixEvent(eventUpdate, room);
|
||||||
|
|
||||||
// Update the room state:
|
// Update the room state:
|
||||||
if (event.stateKey != null &&
|
final stateKey = event.stateKey;
|
||||||
|
if (stateKey != null &&
|
||||||
(!room.partial || importantStateEvents.contains(event.type))) {
|
(!room.partial || importantStateEvents.contains(event.type))) {
|
||||||
room.setState(event);
|
room.setState(event);
|
||||||
|
|
||||||
|
if (room.encrypted &&
|
||||||
|
event.type == EventTypes.RoomMember &&
|
||||||
|
{'join', 'invite'}
|
||||||
|
.contains(event.content.tryGet<String>('membership'))) {
|
||||||
|
// New members should be added to the tracked user IDs for encryption:
|
||||||
|
_trackedUserIds?.add(stateKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type != EventUpdateType.timeline) break;
|
if (type != EventUpdateType.timeline) break;
|
||||||
|
|
||||||
// Is this event redacting the last event?
|
// Is this event redacting the last event?
|
||||||
|
|
@ -3199,13 +3252,11 @@ class Client extends MatrixApi {
|
||||||
for (final room in rooms) {
|
for (final room in rooms) {
|
||||||
if (room.encrypted && room.membership == Membership.join) {
|
if (room.encrypted && room.membership == Membership.join) {
|
||||||
try {
|
try {
|
||||||
final userList = await room.requestParticipants();
|
final userList = await room.requestParticipants(
|
||||||
for (final user in userList) {
|
[Membership.join, Membership.invite],
|
||||||
if ([Membership.join, Membership.invite]
|
true,
|
||||||
.contains(user.membership)) {
|
);
|
||||||
userIds.add(user.id);
|
userIds.addAll(userList.map((user) => user.id));
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('[E2EE] Failed to fetch participants', e, s);
|
Logs().e('[E2EE] Failed to fetch participants', e, s);
|
||||||
}
|
}
|
||||||
|
|
@ -3216,13 +3267,25 @@ class Client extends MatrixApi {
|
||||||
|
|
||||||
final Map<String, DateTime> _keyQueryFailures = {};
|
final Map<String, DateTime> _keyQueryFailures = {};
|
||||||
|
|
||||||
|
/// These are the user IDs we share an encrypted room with and need to track
|
||||||
|
/// the devices from, cached here for performance reasons.
|
||||||
|
/// It gets initialized after the first sync of every
|
||||||
|
/// instance and then updated on member changes or sync device changes.
|
||||||
|
Set<String>? _trackedUserIds;
|
||||||
|
|
||||||
Future<void> updateUserDeviceKeys({Set<String>? additionalUsers}) async {
|
Future<void> updateUserDeviceKeys({Set<String>? additionalUsers}) async {
|
||||||
try {
|
try {
|
||||||
final database = this.database;
|
final database = this.database;
|
||||||
if (!isLogged()) return;
|
if (!isLogged()) return;
|
||||||
final dbActions = <Future<dynamic> Function()>[];
|
final dbActions = <Future<dynamic> Function()>[];
|
||||||
final trackedUserIds = await _getUserIdsInEncryptedRooms();
|
final trackedUserIds =
|
||||||
if (!isLogged()) return;
|
_trackedUserIds ??= await _getUserIdsInEncryptedRooms();
|
||||||
|
if (!isLogged()) {
|
||||||
|
// For the case we get logged out while `_getUserIdsInEncryptedRooms()`
|
||||||
|
// was already started.
|
||||||
|
_trackedUserIds = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
trackedUserIds.add(userID!);
|
trackedUserIds.add(userID!);
|
||||||
if (additionalUsers != null) trackedUserIds.addAll(additionalUsers);
|
if (additionalUsers != null) trackedUserIds.addAll(additionalUsers);
|
||||||
|
|
||||||
|
|
@ -3762,6 +3825,7 @@ class Client extends MatrixApi {
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
await abortSync();
|
await abortSync();
|
||||||
_prevBatch = null;
|
_prevBatch = null;
|
||||||
|
_trackedUserIds = null;
|
||||||
rooms.clear();
|
rooms.clear();
|
||||||
await database.clearCache();
|
await database.clearCache();
|
||||||
encryption?.keyManager.clearOutboundGroupSessions();
|
encryption?.keyManager.clearOutboundGroupSessions();
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import 'package:matrix/src/utils/markdown.dart';
|
||||||
import 'package:matrix/src/utils/multipart_request_progress.dart';
|
import 'package:matrix/src/utils/multipart_request_progress.dart';
|
||||||
|
|
||||||
abstract class RelationshipTypes {
|
abstract class RelationshipTypes {
|
||||||
static const String reply = 'm.in_reply_to';
|
|
||||||
static const String edit = 'm.replace';
|
static const String edit = 'm.replace';
|
||||||
static const String reaction = 'm.annotation';
|
static const String reaction = 'm.annotation';
|
||||||
static const String reference = 'm.reference';
|
static const String reference = 'm.reference';
|
||||||
|
|
@ -490,31 +489,13 @@ class Event extends MatrixEvent {
|
||||||
/// event fallback if the relationship type is `m.thread`.
|
/// event fallback if the relationship type is `m.thread`.
|
||||||
/// https://spec.matrix.org/v1.14/client-server-api/#fallback-for-unthreaded-clients
|
/// https://spec.matrix.org/v1.14/client-server-api/#fallback-for-unthreaded-clients
|
||||||
Future<Event?> getReplyEvent(Timeline timeline) async {
|
Future<Event?> getReplyEvent(Timeline timeline) async {
|
||||||
switch (relationshipType) {
|
final relationshipEventId = content
|
||||||
case RelationshipTypes.reply:
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
final relationshipEventId = this.relationshipEventId;
|
?.tryGetMap<String, Object?>('m.in_reply_to')
|
||||||
return relationshipEventId == null
|
?.tryGet<String>('event_id');
|
||||||
? null
|
return relationshipEventId == null
|
||||||
: await timeline.getEventById(relationshipEventId);
|
? null
|
||||||
|
: await timeline.getEventById(relationshipEventId);
|
||||||
case RelationshipTypes.thread:
|
|
||||||
final relationshipContent =
|
|
||||||
content.tryGetMap<String, Object?>('m.relates_to');
|
|
||||||
if (relationshipContent == null) return null;
|
|
||||||
final String? relationshipEventId;
|
|
||||||
if (relationshipContent.tryGet<bool>('is_falling_back') == true) {
|
|
||||||
relationshipEventId = relationshipContent
|
|
||||||
.tryGetMap<String, Object?>('m.in_reply_to')
|
|
||||||
?.tryGet<String>('event_id');
|
|
||||||
} else {
|
|
||||||
relationshipEventId = this.relationshipEventId;
|
|
||||||
}
|
|
||||||
return relationshipEventId == null
|
|
||||||
? null
|
|
||||||
: await timeline.getEventById(relationshipEventId);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this event is encrypted and the decryption was not successful because
|
/// If this event is encrypted and the decryption was not successful because
|
||||||
|
|
@ -1021,30 +1002,30 @@ class Event extends MatrixEvent {
|
||||||
return transactionId == search;
|
return transactionId == search;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the relationship type of an event. `null` if there is none
|
/// Get the relationship type of an event. `null` if there is none.
|
||||||
String? get relationshipType {
|
String? get relationshipType => content
|
||||||
final mRelatesTo = content.tryGetMap<String, Object?>('m.relates_to');
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
if (mRelatesTo == null) {
|
?.tryGet<String>('rel_type');
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final relType = mRelatesTo.tryGet<String>('rel_type');
|
|
||||||
if (relType == RelationshipTypes.thread) {
|
|
||||||
return RelationshipTypes.thread;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mRelatesTo.containsKey('m.in_reply_to')) {
|
/// Get the event ID that this relationship will reference and `null` if there
|
||||||
return RelationshipTypes.reply;
|
/// is none. This could for example be the thread root, the original event for
|
||||||
}
|
/// an edit or the event, this is an reaction for. For replies please use
|
||||||
return relType;
|
/// `Event.inReplyToEventId()` instead!
|
||||||
}
|
String? get relationshipEventId => content
|
||||||
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
|
?.tryGet<String>('event_id');
|
||||||
|
|
||||||
/// Get the event ID that this relationship will reference. `null` if there is none
|
/// If this event is in reply to another event, this returns the event ID or
|
||||||
String? get relationshipEventId {
|
/// null if this event is not a reply.
|
||||||
final relatesToMap = content.tryGetMap<String, Object?>('m.relates_to');
|
String? inReplyToEventId({bool includingFallback = true}) {
|
||||||
return relatesToMap?.tryGet<String>('event_id') ??
|
final isFallback = content
|
||||||
relatesToMap
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
?.tryGetMap<String, Object?>('m.in_reply_to')
|
?.tryGet<bool>('is_falling_back');
|
||||||
?.tryGet<String>('event_id');
|
if (isFallback == true && !includingFallback) return null;
|
||||||
|
return content
|
||||||
|
.tryGetMap<String, Object?>('m.relates_to')
|
||||||
|
?.tryGetMap<String, Object?>('m.in_reply_to')
|
||||||
|
?.tryGet<String>('event_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get whether this event has aggregated events from a certain [type]
|
/// Get whether this event has aggregated events from a certain [type]
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,7 @@ class Room {
|
||||||
final cache = _cachedDirectChatMatrixId;
|
final cache = _cachedDirectChatMatrixId;
|
||||||
if (cache != null) {
|
if (cache != null) {
|
||||||
final roomIds = client.directChats[cache];
|
final roomIds = client.directChats[cache];
|
||||||
if (roomIds is List && roomIds.contains(id)) {
|
if (roomIds != null && roomIds.contains(id)) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1608,21 +1608,21 @@ class Room {
|
||||||
|
|
||||||
/// Sets this room as a direct chat for this user if not already.
|
/// Sets this room as a direct chat for this user if not already.
|
||||||
Future<void> addToDirectChat(String userID) async {
|
Future<void> addToDirectChat(String userID) async {
|
||||||
final directChats = client.directChats;
|
final dmRooms = List<String>.from(client.directChats[userID] ?? []);
|
||||||
if (directChats[userID] is List) {
|
if (dmRooms.contains(id)) {
|
||||||
if (!directChats[userID].contains(id)) {
|
Logs().d('Already a direct chat.');
|
||||||
directChats[userID].add(id);
|
return;
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
} // Is already in direct chats
|
|
||||||
} else {
|
|
||||||
directChats[userID] = [id];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dmRooms.add(id);
|
||||||
|
|
||||||
await client.setAccountData(
|
await client.setAccountData(
|
||||||
client.userID!,
|
client.userID!,
|
||||||
'm.direct',
|
'm.direct',
|
||||||
directChats,
|
{
|
||||||
|
...client.directChats,
|
||||||
|
userID: dmRooms,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2526,18 +2526,30 @@ class Room {
|
||||||
JoinRules joinRules, {
|
JoinRules joinRules, {
|
||||||
/// For restricted rooms, the id of the room where a user needs to be member.
|
/// For restricted rooms, the id of the room where a user needs to be member.
|
||||||
/// Learn more at https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
/// Learn more at https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||||
|
List<String>? allowConditionRoomIds,
|
||||||
|
@Deprecated('Use allowConditionRoomIds instead!')
|
||||||
String? allowConditionRoomId,
|
String? allowConditionRoomId,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (allowConditionRoomId != null) {
|
||||||
|
allowConditionRoomIds ??= [];
|
||||||
|
allowConditionRoomIds.add(allowConditionRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
await client.setRoomStateWithKey(
|
await client.setRoomStateWithKey(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomJoinRules,
|
EventTypes.RoomJoinRules,
|
||||||
'',
|
'',
|
||||||
{
|
{
|
||||||
'join_rule': joinRules.toString().replaceAll('JoinRules.', ''),
|
'join_rule': joinRules.text,
|
||||||
if (allowConditionRoomId != null)
|
if (allowConditionRoomIds != null && allowConditionRoomIds.isNotEmpty)
|
||||||
'allow': [
|
'allow': allowConditionRoomIds
|
||||||
{'room_id': allowConditionRoomId, 'type': 'm.room_membership'},
|
.map(
|
||||||
],
|
(allowConditionRoomId) => {
|
||||||
|
'room_id': allowConditionRoomId,
|
||||||
|
'type': 'm.room_membership',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -2779,10 +2791,21 @@ class Room {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a child from this space by setting the `via` to an empty list.
|
/// Remove a child from this space by removing the space child and optionally
|
||||||
Future<void> removeSpaceChild(String roomId) => !isSpace
|
/// space parent state events.
|
||||||
? throw Exception('Room is not a space!')
|
Future<void> removeSpaceChild(String roomId) async {
|
||||||
: setSpaceChild(roomId, via: const []);
|
if (!isSpace) throw Exception('Room is not a space!');
|
||||||
|
|
||||||
|
await client.setRoomStateWithKey(id, EventTypes.SpaceChild, roomId, {});
|
||||||
|
|
||||||
|
// Optionally remove the space parent state event in the former space child.
|
||||||
|
if (client
|
||||||
|
.getRoomById(roomId)
|
||||||
|
?.canChangeStateEvent(EventTypes.SpaceParent) ==
|
||||||
|
true) {
|
||||||
|
await client.setRoomStateWithKey(roomId, EventTypes.SpaceParent, id, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => (other is Room && other.id == id);
|
bool operator ==(Object other) => (other is Room && other.id == id);
|
||||||
|
|
|
||||||
|
|
@ -300,5 +300,9 @@ abstract class EventLocalizations {
|
||||||
body,
|
body,
|
||||||
),
|
),
|
||||||
EventTypes.refreshingLastEvent: (_, i18n, ___) => i18n.refreshingLastEvent,
|
EventTypes.refreshingLastEvent: (_, i18n, ___) => i18n.refreshingLastEvent,
|
||||||
|
PollEventContent.startType: (event, i18n, body) => i18n.startedAPoll(
|
||||||
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
|
PollEventContent.endType: (event, i18n, body) => i18n.pollHasBeenEnded,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ class HtmlToText {
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _listBulletPoints = <String>['●', '○', '■', '‣'];
|
static const _listBulletPoints = <String>['•', '◦', '▪', '‣'];
|
||||||
|
|
||||||
static List<String> _listChildNodes(
|
static List<String> _listChildNodes(
|
||||||
_ConvertOpts opts,
|
_ConvertOpts opts,
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,13 @@ String markdown(
|
||||||
bool convertLinebreaks = true,
|
bool convertLinebreaks = true,
|
||||||
}) {
|
}) {
|
||||||
var ret = markdownToHtml(
|
var ret = markdownToHtml(
|
||||||
text.replaceNewlines(),
|
text
|
||||||
|
.replaceAllMapped(
|
||||||
|
// Replace HTML tags
|
||||||
|
RegExp(r'<([^>]*)>'),
|
||||||
|
(match) => '<${match.group(1)}>',
|
||||||
|
)
|
||||||
|
.replaceNewlines(),
|
||||||
extensionSet: ExtensionSet.gitHubFlavored,
|
extensionSet: ExtensionSet.gitHubFlavored,
|
||||||
blockSyntaxes: [
|
blockSyntaxes: [
|
||||||
BlockLatexSyntax(),
|
BlockLatexSyntax(),
|
||||||
|
|
|
||||||
|
|
@ -321,4 +321,10 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get refreshingLastEvent => 'Refreshing last event...';
|
String get refreshingLastEvent => 'Refreshing last event...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String startedAPoll(String senderName) => '$senderName started a poll';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pollHasBeenEnded => 'Poll has been ended';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,10 @@ abstract class MatrixLocalizations {
|
||||||
String completedKeyVerification(String senderName);
|
String completedKeyVerification(String senderName);
|
||||||
|
|
||||||
String canceledKeyVerification(String senderName);
|
String canceledKeyVerification(String senderName);
|
||||||
|
|
||||||
|
String startedAPoll(String senderName);
|
||||||
|
|
||||||
|
String get pollHasBeenEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HistoryVisibilityDisplayString on HistoryVisibility {
|
extension HistoryVisibilityDisplayString on HistoryVisibility {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,9 @@ class MeshBackend extends CallBackend {
|
||||||
Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
|
Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
|
||||||
_callSessions.add(call);
|
_callSessions.add(call);
|
||||||
_initCall(groupCall, call);
|
_initCall(groupCall, call);
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
||||||
|
groupCall.matrixRTCEventStream.add(CallAddedEvent(call));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// init a peer call from group calls.
|
/// init a peer call from group calls.
|
||||||
|
|
@ -183,7 +185,10 @@ class MeshBackend extends CallBackend {
|
||||||
_registerListenersBeforeCallAdd(replacementCall);
|
_registerListenersBeforeCallAdd(replacementCall);
|
||||||
_initCall(groupCall, replacementCall);
|
_initCall(groupCall, replacementCall);
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(CallReplacedEvent(existingCall, replacementCall));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a peer call from group calls.
|
/// Removes a peer call from group calls.
|
||||||
|
|
@ -196,7 +201,9 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
_callSessions.removeWhere((element) => call.callId == element.callId);
|
_callSessions.removeWhere((element) => call.callId == element.callId);
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
||||||
|
groupCall.matrixRTCEventStream.add(CallRemovedEvent(call));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _disposeCall(
|
Future<void> _disposeCall(
|
||||||
|
|
@ -375,7 +382,10 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
if (nextActiveSpeaker != null && _activeSpeaker != nextActiveSpeaker) {
|
if (nextActiveSpeaker != null && _activeSpeaker != nextActiveSpeaker) {
|
||||||
_activeSpeaker = nextActiveSpeaker;
|
_activeSpeaker = nextActiveSpeaker;
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallActiveSpeakerChanged(_activeSpeaker!));
|
||||||
}
|
}
|
||||||
_activeSpeakerLoopTimeout?.cancel();
|
_activeSpeakerLoopTimeout?.cancel();
|
||||||
_activeSpeakerLoopTimeout = Timer(
|
_activeSpeakerLoopTimeout = Timer(
|
||||||
|
|
@ -401,8 +411,12 @@ class MeshBackend extends CallBackend {
|
||||||
) {
|
) {
|
||||||
_screenshareStreams.add(stream);
|
_screenshareStreams.add(stream);
|
||||||
onStreamAdd.add(stream);
|
onStreamAdd.add(stream);
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.screenshareStreamsChanged);
|
.add(GroupCallStateChange.screenshareStreamsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStreamAdded(GroupCallStreamType.screenshare));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _replaceScreenshareStream(
|
Future<void> _replaceScreenshareStream(
|
||||||
|
|
@ -423,8 +437,12 @@ class MeshBackend extends CallBackend {
|
||||||
_screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
_screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
||||||
|
|
||||||
await existingStream.dispose();
|
await existingStream.dispose();
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.screenshareStreamsChanged);
|
.add(GroupCallStateChange.screenshareStreamsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStreamReplaced(GroupCallStreamType.screenshare));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _removeScreenshareStream(
|
Future<void> _removeScreenshareStream(
|
||||||
|
|
@ -450,8 +468,12 @@ class MeshBackend extends CallBackend {
|
||||||
await stopMediaStream(stream.stream);
|
await stopMediaStream(stream.stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.screenshareStreamsChanged);
|
.add(GroupCallStateChange.screenshareStreamsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStreamRemoved(GroupCallStreamType.screenshare));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onCallStateChanged(CallSession call, CallState state) async {
|
Future<void> _onCallStateChanged(CallSession call, CallState state) async {
|
||||||
|
|
@ -486,8 +508,12 @@ class MeshBackend extends CallBackend {
|
||||||
) async {
|
) async {
|
||||||
_userMediaStreams.add(stream);
|
_userMediaStreams.add(stream);
|
||||||
onStreamAdd.add(stream);
|
onStreamAdd.add(stream);
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.userMediaStreamsChanged);
|
.add(GroupCallStateChange.userMediaStreamsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStreamAdded(GroupCallStreamType.userMedia));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _replaceUserMediaStream(
|
Future<void> _replaceUserMediaStream(
|
||||||
|
|
@ -508,8 +534,12 @@ class MeshBackend extends CallBackend {
|
||||||
_userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
_userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
||||||
|
|
||||||
await existingStream.dispose();
|
await existingStream.dispose();
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.userMediaStreamsChanged);
|
.add(GroupCallStateChange.userMediaStreamsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStreamReplaced(GroupCallStreamType.userMedia));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _removeUserMediaStream(
|
Future<void> _removeUserMediaStream(
|
||||||
|
|
@ -536,12 +566,19 @@ class MeshBackend extends CallBackend {
|
||||||
await stopMediaStream(stream.stream);
|
await stopMediaStream(stream.stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.userMediaStreamsChanged);
|
.add(GroupCallStateChange.userMediaStreamsChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStreamRemoved(GroupCallStreamType.userMedia));
|
||||||
|
|
||||||
if (_activeSpeaker == stream.participant && _userMediaStreams.isNotEmpty) {
|
if (_activeSpeaker == stream.participant && _userMediaStreams.isNotEmpty) {
|
||||||
_activeSpeaker = _userMediaStreams[0].participant;
|
_activeSpeaker = _userMediaStreams[0].participant;
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallActiveSpeakerChanged(_activeSpeaker!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,7 +700,9 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.localMuteStateChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.localMuteStateChanged);
|
||||||
|
groupCall.matrixRTCEventStream.add(GroupCallLocalMutedChanged(muted, kind));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -799,8 +838,12 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
_addScreenshareStream(groupCall, localScreenshareStream!);
|
_addScreenshareStream(groupCall, localScreenshareStream!);
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.localScreenshareStateChanged);
|
.add(GroupCallStateChange.localScreenshareStateChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallLocalScreenshareStateChanged(true));
|
||||||
for (final call in _callSessions) {
|
for (final call in _callSessions) {
|
||||||
await call.addLocalStream(
|
await call.addLocalStream(
|
||||||
await localScreenshareStream!.stream!.clone(),
|
await localScreenshareStream!.stream!.clone(),
|
||||||
|
|
@ -813,7 +856,10 @@ class MeshBackend extends CallBackend {
|
||||||
return;
|
return;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('[VOIP] Enabling screensharing error', e, s);
|
Logs().e('[VOIP] Enabling screensharing error', e, s);
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.error);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.error);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallStateError(e.toString(), s));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -826,8 +872,12 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
await groupCall.sendMemberStateEvent();
|
await groupCall.sendMemberStateEvent();
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
.add(GroupCallStateChange.localMuteStateChanged);
|
.add(GroupCallStateChange.localMuteStateChanged);
|
||||||
|
groupCall.matrixRTCEventStream
|
||||||
|
.add(GroupCallLocalScreenshareStateChanged(false));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,11 @@ class GroupCallSession {
|
||||||
|
|
||||||
String groupCallId;
|
String groupCallId;
|
||||||
|
|
||||||
|
@Deprecated('Use matrixRTCEventStream instead')
|
||||||
final CachedStreamController<GroupCallState> onGroupCallState =
|
final CachedStreamController<GroupCallState> onGroupCallState =
|
||||||
CachedStreamController();
|
CachedStreamController();
|
||||||
|
|
||||||
|
@Deprecated('Use matrixRTCEventStream instead')
|
||||||
final CachedStreamController<GroupCallStateChange> onGroupCallEvent =
|
final CachedStreamController<GroupCallStateChange> onGroupCallEvent =
|
||||||
CachedStreamController();
|
CachedStreamController();
|
||||||
|
|
||||||
|
|
@ -105,8 +107,11 @@ class GroupCallSession {
|
||||||
|
|
||||||
void setState(GroupCallState newState) {
|
void setState(GroupCallState newState) {
|
||||||
state = newState;
|
state = newState;
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
onGroupCallState.add(newState);
|
onGroupCallState.add(newState);
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
onGroupCallEvent.add(GroupCallStateChange.groupCallStateChanged);
|
onGroupCallEvent.add(GroupCallStateChange.groupCallStateChanged);
|
||||||
|
matrixRTCEventStream.add(GroupCallStateChanged(newState));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasLocalParticipant() {
|
bool hasLocalParticipant() {
|
||||||
|
|
@ -313,6 +318,7 @@ class GroupCallSession {
|
||||||
.add(ParticipantsLeftEvent(participants: anyLeft.toList()));
|
.add(ParticipantsLeftEvent(participants: anyLeft.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
|
onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,18 @@ import 'package:matrix/matrix.dart';
|
||||||
/// often.
|
/// often.
|
||||||
sealed class MatrixRTCCallEvent {}
|
sealed class MatrixRTCCallEvent {}
|
||||||
|
|
||||||
|
/// Event type for participants change
|
||||||
sealed class ParticipantsChangeEvent implements MatrixRTCCallEvent {}
|
sealed class ParticipantsChangeEvent implements MatrixRTCCallEvent {}
|
||||||
|
|
||||||
final class ParticipantsJoinEvent implements ParticipantsChangeEvent {
|
final class ParticipantsJoinEvent implements ParticipantsChangeEvent {
|
||||||
|
/// The participants who joined the call
|
||||||
final List<CallParticipant> participants;
|
final List<CallParticipant> participants;
|
||||||
|
|
||||||
ParticipantsJoinEvent({required this.participants});
|
ParticipantsJoinEvent({required this.participants});
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ParticipantsLeftEvent implements ParticipantsChangeEvent {
|
final class ParticipantsLeftEvent implements ParticipantsChangeEvent {
|
||||||
|
/// The participants who left the call
|
||||||
final List<CallParticipant> participants;
|
final List<CallParticipant> participants;
|
||||||
|
|
||||||
ParticipantsLeftEvent({required this.participants});
|
ParticipantsLeftEvent({required this.participants});
|
||||||
|
|
@ -46,3 +49,89 @@ final class CallReactionRemovedEvent implements CallReactionEvent {
|
||||||
required this.redactedEventId,
|
required this.redactedEventId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Group call active speaker changed event
|
||||||
|
final class GroupCallActiveSpeakerChanged implements MatrixRTCCallEvent {
|
||||||
|
final CallParticipant participant;
|
||||||
|
GroupCallActiveSpeakerChanged(this.participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group calls changed event type
|
||||||
|
sealed class GroupCallChanged implements MatrixRTCCallEvent {}
|
||||||
|
|
||||||
|
/// Group call, call added event
|
||||||
|
final class CallAddedEvent implements GroupCallChanged {
|
||||||
|
final CallSession call;
|
||||||
|
CallAddedEvent(this.call);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call, call removed event
|
||||||
|
final class CallRemovedEvent implements GroupCallChanged {
|
||||||
|
final CallSession call;
|
||||||
|
CallRemovedEvent(this.call);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call, call replaced event
|
||||||
|
final class CallReplacedEvent extends GroupCallChanged {
|
||||||
|
final CallSession existingCall, replacementCall;
|
||||||
|
CallReplacedEvent(this.existingCall, this.replacementCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GroupCallStreamType {
|
||||||
|
userMedia,
|
||||||
|
screenshare,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call stream added event
|
||||||
|
final class GroupCallStreamAdded implements MatrixRTCCallEvent {
|
||||||
|
final GroupCallStreamType type;
|
||||||
|
GroupCallStreamAdded(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call stream removed event
|
||||||
|
final class GroupCallStreamRemoved implements MatrixRTCCallEvent {
|
||||||
|
final GroupCallStreamType type;
|
||||||
|
GroupCallStreamRemoved(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call stream replaced event
|
||||||
|
final class GroupCallStreamReplaced implements MatrixRTCCallEvent {
|
||||||
|
final GroupCallStreamType type;
|
||||||
|
GroupCallStreamReplaced(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call local screenshare state changed event
|
||||||
|
final class GroupCallLocalScreenshareStateChanged
|
||||||
|
implements MatrixRTCCallEvent {
|
||||||
|
final bool screensharing;
|
||||||
|
GroupCallLocalScreenshareStateChanged(this.screensharing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call local muted changed event
|
||||||
|
final class GroupCallLocalMutedChanged implements MatrixRTCCallEvent {
|
||||||
|
final bool muted;
|
||||||
|
final MediaInputKind kind;
|
||||||
|
GroupCallLocalMutedChanged(this.muted, this.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GroupCallState {
|
||||||
|
localCallFeedUninitialized,
|
||||||
|
initializingLocalCallFeed,
|
||||||
|
localCallFeedInitialized,
|
||||||
|
entering,
|
||||||
|
entered,
|
||||||
|
ended
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call state changed event
|
||||||
|
final class GroupCallStateChanged implements MatrixRTCCallEvent {
|
||||||
|
final GroupCallState state;
|
||||||
|
GroupCallStateChanged(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group call error event
|
||||||
|
final class GroupCallStateError implements MatrixRTCCallEvent {
|
||||||
|
final String msg;
|
||||||
|
final dynamic err;
|
||||||
|
GroupCallStateError(this.msg, this.err);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ class GroupCallError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Use the events implementing MatrixRTCCallEvent instead')
|
||||||
enum GroupCallStateChange {
|
enum GroupCallStateChange {
|
||||||
groupCallStateChanged,
|
groupCallStateChanged,
|
||||||
activeSpeakerChanged,
|
activeSpeakerChanged,
|
||||||
|
|
@ -176,12 +177,3 @@ enum GroupCallStateChange {
|
||||||
participantsChanged,
|
participantsChanged,
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GroupCallState {
|
|
||||||
localCallFeedUninitialized,
|
|
||||||
initializingLocalCallFeed,
|
|
||||||
localCallFeedInitialized,
|
|
||||||
entering,
|
|
||||||
entered,
|
|
||||||
ended
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: matrix
|
name: matrix
|
||||||
description: Matrix Dart SDK
|
description: Matrix Dart SDK
|
||||||
version: 3.0.1
|
version: 4.0.0
|
||||||
homepage: https://famedly.com
|
homepage: https://famedly.com
|
||||||
repository: https://github.com/famedly/matrix-dart-sdk.git
|
repository: https://github.com/famedly/matrix-dart-sdk.git
|
||||||
issue_tracker: https://github.com/famedly/matrix-dart-sdk/issues
|
issue_tracker: https://github.com/famedly/matrix-dart-sdk/issues
|
||||||
|
|
@ -27,7 +27,7 @@ dependencies:
|
||||||
sqflite_common: ^2.4.5
|
sqflite_common: ^2.4.5
|
||||||
sqlite3: ^2.1.0
|
sqlite3: ^2.1.0
|
||||||
typed_data: ^1.3.2
|
typed_data: ^1.3.2
|
||||||
vodozemac: ^0.3.0
|
vodozemac: ^0.4.0
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
webrtc_interface: ^1.2.0
|
webrtc_interface: ^1.2.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -524,6 +524,22 @@ void main() {
|
||||||
FakeMatrixApi.currentApi?.api = oldapi!;
|
FakeMatrixApi.currentApi?.api = oldapi!;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('init() with access token', () async {
|
||||||
|
final client = Client(
|
||||||
|
'testclient',
|
||||||
|
httpClient: FakeMatrixApi(),
|
||||||
|
database: await getDatabase(),
|
||||||
|
);
|
||||||
|
await client.init(
|
||||||
|
newToken: 'abcd1234',
|
||||||
|
newHomeserver: Uri.parse('https://fakeserver.notexisting'),
|
||||||
|
);
|
||||||
|
expect(client.isLogged(), true);
|
||||||
|
expect(client.userID, 'alice@example.com');
|
||||||
|
expect(client.deviceID, 'ABCDEFGH');
|
||||||
|
await client.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('Login', () async {
|
test('Login', () async {
|
||||||
matrix = Client(
|
matrix = Client(
|
||||||
'testclient',
|
'testclient',
|
||||||
|
|
@ -531,9 +547,14 @@ void main() {
|
||||||
database: await getDatabase(),
|
database: await getDatabase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await matrix.checkHomeserver(
|
final (_, _, _, authMetadata) = await matrix.checkHomeserver(
|
||||||
Uri.parse('https://fakeserver.notexisting'),
|
Uri.parse('https://fakeserver.notexisting'),
|
||||||
checkWellKnown: false,
|
checkWellKnown: false,
|
||||||
|
fetchAuthMetadata: true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
authMetadata?.issuer.toString(),
|
||||||
|
'https://fakeserver.notexisting/',
|
||||||
);
|
);
|
||||||
|
|
||||||
final loginResp = await matrix.login(
|
final loginResp = await matrix.login(
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ void main() async {
|
||||||
expect(event.formattedText, formatted_body);
|
expect(event.formattedText, formatted_body);
|
||||||
expect(event.body, body);
|
expect(event.body, body);
|
||||||
expect(event.type, EventTypes.Message);
|
expect(event.type, EventTypes.Message);
|
||||||
expect(event.relationshipType, RelationshipTypes.reply);
|
expect(event.inReplyToEventId(), '\$1234:example.com');
|
||||||
jsonObj['state_key'] = '';
|
jsonObj['state_key'] = '';
|
||||||
final state = Event.fromJson(jsonObj, room);
|
final state = Event.fromJson(jsonObj, room);
|
||||||
expect(state.eventId, id);
|
expect(state.eventId, id);
|
||||||
|
|
@ -178,8 +178,8 @@ void main() async {
|
||||||
};
|
};
|
||||||
event = Event.fromJson(jsonObj, room);
|
event = Event.fromJson(jsonObj, room);
|
||||||
expect(event.messageType, MessageTypes.Text);
|
expect(event.messageType, MessageTypes.Text);
|
||||||
expect(event.relationshipType, RelationshipTypes.reply);
|
expect(event.inReplyToEventId(), '1234');
|
||||||
expect(event.relationshipEventId, '1234');
|
expect(event.relationshipEventId, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('relationship types', () async {
|
test('relationship types', () async {
|
||||||
|
|
@ -212,8 +212,22 @@ void main() async {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
event = Event.fromJson(jsonObj, room);
|
event = Event.fromJson(jsonObj, room);
|
||||||
expect(event.relationshipType, RelationshipTypes.reply);
|
expect(event.inReplyToEventId(), 'def');
|
||||||
expect(event.relationshipEventId, 'def');
|
expect(event.relationshipEventId, null);
|
||||||
|
|
||||||
|
jsonObj['content']['m.relates_to'] = {
|
||||||
|
'rel_type': 'm.thread',
|
||||||
|
'event_id': '\$root',
|
||||||
|
'm.in_reply_to': {
|
||||||
|
'event_id': '\$target',
|
||||||
|
},
|
||||||
|
'is_falling_back': true,
|
||||||
|
};
|
||||||
|
event = Event.fromJson(jsonObj, room);
|
||||||
|
expect(event.relationshipType, RelationshipTypes.thread);
|
||||||
|
expect(event.inReplyToEventId(), '\$target');
|
||||||
|
expect(event.inReplyToEventId(includingFallback: false), null);
|
||||||
|
expect(event.relationshipEventId, '\$root');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('redact', () async {
|
test('redact', () async {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ void main() {
|
||||||
'(cw spiders) ███████████████████████',
|
'(cw spiders) ███████████████████████',
|
||||||
'<img src="test.gif" alt="a test case" />': 'a test case',
|
'<img src="test.gif" alt="a test case" />': 'a test case',
|
||||||
'List of cute animals:\n<ul>\n<li>Kittens</li>\n<li>Puppies</li>\n<li>Snakes<br/>(I think they\'re cute!)</li>\n</ul>\n(This list is incomplete, you can help by adding to it!)':
|
'List of cute animals:\n<ul>\n<li>Kittens</li>\n<li>Puppies</li>\n<li>Snakes<br/>(I think they\'re cute!)</li>\n</ul>\n(This list is incomplete, you can help by adding to it!)':
|
||||||
'List of cute animals:\n● Kittens\n● Puppies\n● Snakes\n (I think they\'re cute!)\n(This list is incomplete, you can help by adding to it!)',
|
'List of cute animals:\n• Kittens\n• Puppies\n• Snakes\n (I think they\'re cute!)\n(This list is incomplete, you can help by adding to it!)',
|
||||||
'<em>fox</em>': '*fox*',
|
'<em>fox</em>': '*fox*',
|
||||||
'<i>fox</i>': '*fox*',
|
'<i>fox</i>': '*fox*',
|
||||||
'<strong>fox</i>': '**fox**',
|
'<strong>fox</i>': '**fox**',
|
||||||
|
|
@ -67,15 +67,15 @@ void main() {
|
||||||
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
|
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
|
||||||
'> > fox\n> floof\nfluff',
|
'> > fox\n> floof\nfluff',
|
||||||
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
|
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
|
||||||
'● hey\n ○ a\n ○ b\n● foxies',
|
'• hey\n ◦ a\n ◦ b\n• foxies',
|
||||||
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
|
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
|
||||||
'<ol start="42"><li>a</li><li>b</li></ol>': '42. a\n43. b',
|
'<ol start="42"><li>a</li><li>b</li></ol>': '42. a\n43. b',
|
||||||
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
|
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
|
||||||
'1. a\n 1. aa\n 2. bb\n2. b',
|
'1. a\n 1. aa\n 2. bb\n2. b',
|
||||||
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
|
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
|
||||||
'1. a\n ○ aa\n ○ bb\n2. b',
|
'1. a\n ◦ aa\n ◦ bb\n2. b',
|
||||||
'<ul><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ul>':
|
'<ul><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ul>':
|
||||||
'● a\n 1. aa\n 2. bb\n● b',
|
'• a\n 1. aa\n 2. bb\n• b',
|
||||||
'<mx-reply>bunny</mx-reply>fox': 'fox',
|
'<mx-reply>bunny</mx-reply>fox': 'fox',
|
||||||
'fox<hr>floof': 'fox\n----------\nfloof',
|
'fox<hr>floof': 'fox\n----------\nfloof',
|
||||||
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
|
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,10 @@ void main() {
|
||||||
),
|
),
|
||||||
'<p>The first<br/>codeblock</p><pre><code class="language-dart">void main(){\nprint(something);\n}\n</code></pre><p>And the second code block</p><pre><code class="language-js">meow\nmeow\n</code></pre>',
|
'<p>The first<br/>codeblock</p><pre><code class="language-dart">void main(){\nprint(something);\n}\n</code></pre><p>And the second code block</p><pre><code class="language-js">meow\nmeow\n</code></pre>',
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
markdown('Test <m> *unescaped*'),
|
||||||
|
'Test <m> <em>unescaped</em>',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
test('Checkboxes', () {
|
test('Checkboxes', () {
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,123 @@
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:matrix/src/models/timeline_chunk.dart';
|
||||||
|
import '../fake_client.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('MSC 3881 Polls', () {
|
||||||
|
late Client client;
|
||||||
|
const roomId = '!696r7674:example.com';
|
||||||
|
setUpAll(() async {
|
||||||
|
client = await getClient();
|
||||||
|
});
|
||||||
|
tearDownAll(() async => client.dispose());
|
||||||
|
test('Start poll', () async {
|
||||||
|
final room = client.getRoomById(roomId)!;
|
||||||
|
final eventId = await room.startPoll(
|
||||||
|
question: 'What do you like more?',
|
||||||
|
kind: PollKind.undisclosed,
|
||||||
|
maxSelections: 2,
|
||||||
|
answers: [
|
||||||
|
PollAnswer(
|
||||||
|
id: 'pepsi',
|
||||||
|
mText: 'Pepsi',
|
||||||
|
),
|
||||||
|
PollAnswer(
|
||||||
|
id: 'coca',
|
||||||
|
mText: 'Coca Cola',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
txid: '1234',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(eventId, '1234');
|
||||||
|
});
|
||||||
|
test('Check Poll Event', () async {
|
||||||
|
final room = client.getRoomById(roomId)!;
|
||||||
|
final pollEventContent = PollEventContent(
|
||||||
|
mText: 'TestPoll',
|
||||||
|
pollStartContent: PollStartContent(
|
||||||
|
maxSelections: 2,
|
||||||
|
question: PollQuestion(mText: 'Question'),
|
||||||
|
answers: [PollAnswer(id: 'id', mText: 'mText')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final pollEvent = Event(
|
||||||
|
content: pollEventContent.toJson(),
|
||||||
|
type: PollEventContent.startType,
|
||||||
|
eventId: 'testevent',
|
||||||
|
senderId: client.userID!,
|
||||||
|
originServerTs: DateTime.now().subtract(const Duration(seconds: 10)),
|
||||||
|
room: room,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
pollEvent.parsedPollEventContent.toJson(),
|
||||||
|
pollEventContent.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final timeline = Timeline(
|
||||||
|
room: room,
|
||||||
|
chunk: TimelineChunk(
|
||||||
|
events: [pollEvent],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pollEvent.getPollResponses(timeline), {});
|
||||||
|
expect(pollEvent.getPollHasBeenEnded(timeline), false);
|
||||||
|
|
||||||
|
timeline.aggregatedEvents['testevent'] ??= {};
|
||||||
|
timeline.aggregatedEvents['testevent']?['m.reference'] ??= {};
|
||||||
|
|
||||||
|
timeline.aggregatedEvents['testevent']!['m.reference']!.add(
|
||||||
|
Event(
|
||||||
|
content: {
|
||||||
|
'm.relates_to': {
|
||||||
|
'rel_type': 'm.reference',
|
||||||
|
'event_id': 'testevent',
|
||||||
|
},
|
||||||
|
'org.matrix.msc3381.poll.response': {
|
||||||
|
'answers': ['pepsi'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: PollEventContent.responseType,
|
||||||
|
eventId: 'testevent2',
|
||||||
|
senderId: client.userID!,
|
||||||
|
originServerTs: DateTime.now().subtract(const Duration(seconds: 9)),
|
||||||
|
room: room,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
pollEvent.getPollResponses(timeline),
|
||||||
|
{
|
||||||
|
'@test:fakeServer.notExisting': ['pepsi'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
timeline.aggregatedEvents['testevent']!['m.reference']!.add(
|
||||||
|
Event(
|
||||||
|
content: {
|
||||||
|
'm.relates_to': {
|
||||||
|
'rel_type': 'm.reference',
|
||||||
|
'event_id': 'testevent',
|
||||||
|
},
|
||||||
|
'org.matrix.msc3381.poll.end': {},
|
||||||
|
},
|
||||||
|
type: PollEventContent.responseType,
|
||||||
|
eventId: 'testevent3',
|
||||||
|
senderId: client.userID!,
|
||||||
|
originServerTs: DateTime.now().subtract(const Duration(seconds: 8)),
|
||||||
|
room: room,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(pollEvent.getPollHasBeenEnded(timeline), true);
|
||||||
|
|
||||||
|
final respondeEventId = await pollEvent.answerPoll(
|
||||||
|
['pepsi'],
|
||||||
|
txid: '1234',
|
||||||
|
);
|
||||||
|
expect(respondeEventId, '1234');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -139,6 +139,7 @@ void main() async {
|
||||||
await user1.setPower(50);
|
await user1.setPower(50);
|
||||||
});
|
});
|
||||||
test('startDirectChat', () async {
|
test('startDirectChat', () async {
|
||||||
|
FakeMatrixApi.client = user1.room.client;
|
||||||
await user1.startDirectChat(waitForSync: false);
|
await user1.startDirectChat(waitForSync: false);
|
||||||
});
|
});
|
||||||
test('getPresence', () async {
|
test('getPresence', () async {
|
||||||
|
|
|
||||||
|
|
@ -86,18 +86,49 @@ class MockEncryptionKeyProvider implements EncryptionKeyProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockMediaDeviceInfo implements MediaDeviceInfo {
|
||||||
|
@override
|
||||||
|
final String deviceId;
|
||||||
|
@override
|
||||||
|
final String kind;
|
||||||
|
@override
|
||||||
|
final String label;
|
||||||
|
@override
|
||||||
|
final String? groupId;
|
||||||
|
|
||||||
|
MockMediaDeviceInfo({
|
||||||
|
required this.deviceId,
|
||||||
|
required this.kind,
|
||||||
|
required this.label,
|
||||||
|
this.groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class MockMediaDevices implements MediaDevices {
|
class MockMediaDevices implements MediaDevices {
|
||||||
@override
|
@override
|
||||||
Function(dynamic event)? ondevicechange;
|
Function(dynamic event)? ondevicechange;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<MediaDeviceInfo>> enumerateDevices() {
|
Future<List<MediaDeviceInfo>> enumerateDevices() async {
|
||||||
throw UnimplementedError();
|
return [
|
||||||
|
MockMediaDeviceInfo(
|
||||||
|
deviceId: 'default_audio_input',
|
||||||
|
kind: 'audioinput',
|
||||||
|
label: 'Default Audio Input',
|
||||||
|
),
|
||||||
|
MockMediaDeviceInfo(
|
||||||
|
deviceId: 'default_video_input',
|
||||||
|
kind: 'videoinput',
|
||||||
|
label: 'Default Video Input',
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MediaStream> getDisplayMedia(Map<String, dynamic> mediaConstraints) {
|
Future<MediaStream> getDisplayMedia(
|
||||||
throw UnimplementedError();
|
Map<String, dynamic> mediaConstraints,
|
||||||
|
) async {
|
||||||
|
return MockMediaStream('', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -160,6 +191,9 @@ class MockRTCPeerConnection implements RTCPeerConnection {
|
||||||
@override
|
@override
|
||||||
Function(RTCTrackEvent event)? onTrack;
|
Function(RTCTrackEvent event)? onTrack;
|
||||||
|
|
||||||
|
// Mock stats to simulate audio levels
|
||||||
|
double mockAudioLevel = 0.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RTCSignalingState? get signalingState => throw UnimplementedError();
|
RTCSignalingState? get signalingState => throw UnimplementedError();
|
||||||
|
|
||||||
|
|
@ -276,8 +310,23 @@ class MockRTCPeerConnection implements RTCPeerConnection {
|
||||||
@override
|
@override
|
||||||
Future<List<StatsReport>> getStats([MediaStreamTrack? track]) async {
|
Future<List<StatsReport>> getStats([MediaStreamTrack? track]) async {
|
||||||
// Mock implementation for getting stats
|
// Mock implementation for getting stats
|
||||||
Logs().i('Mock: Getting stats');
|
Logs().i('Mock: Getting stats with audioLevel: $mockAudioLevel');
|
||||||
return [];
|
return [
|
||||||
|
MockStatsReport(
|
||||||
|
type: 'inbound-rtp',
|
||||||
|
values: {
|
||||||
|
'kind': 'audio',
|
||||||
|
'audioLevel': mockAudioLevel,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MockStatsReport(
|
||||||
|
type: 'media-source',
|
||||||
|
values: {
|
||||||
|
'kind': 'audio',
|
||||||
|
'audioLevel': mockAudioLevel,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -850,3 +899,45 @@ class MockVideoRenderer implements VideoRenderer {
|
||||||
Logs().i('Mock: Disposing VideoRenderer');
|
Logs().i('Mock: Disposing VideoRenderer');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockStatsReport implements StatsReport {
|
||||||
|
@override
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Map<String, dynamic> values;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double timestamp;
|
||||||
|
|
||||||
|
MockStatsReport({
|
||||||
|
required this.type,
|
||||||
|
required this.values,
|
||||||
|
this.id = 'mock-stats-id',
|
||||||
|
this.timestamp = 0.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockRTCTrackEvent implements RTCTrackEvent {
|
||||||
|
@override
|
||||||
|
final MediaStreamTrack track;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final RTCRtpReceiver? receiver;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final List<MediaStream> streams;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final RTCRtpTransceiver? transceiver;
|
||||||
|
|
||||||
|
MockRTCTrackEvent({
|
||||||
|
required this.track,
|
||||||
|
this.receiver,
|
||||||
|
required this.streams,
|
||||||
|
this.transceiver,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue