Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6.4.9 #1360

Merged
merged 49 commits into from
Jan 11, 2025
Merged

6.4.9 #1360

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
684e4e5
Revert "Use christmas logo"
tmolitor-stud-tu Dec 31, 2024
d798d54
Don't pile up already fired ping request delaying timers
tmolitor-stud-tu Dec 24, 2024
78f221b
Add more logging to xmpp.m
tmolitor-stud-tu Dec 24, 2024
3dbfe20
Fix weird apple foo creating a second (unusable) framer
tmolitor-stud-tu Dec 24, 2024
9dad64e
Fix KVOObserver usage in ContactEntry
tmolitor-stud-tu Dec 27, 2024
3f5e630
Fix crash on LMC replace without id (spec violation!)
tmolitor-stud-tu Dec 31, 2024
e38e8cf
Send out log messages via UDP even if global logging queue is suspended
tmolitor-stud-tu Dec 29, 2024
ab22e7c
Fix weird double shutdown bug
tmolitor-stud-tu Dec 29, 2024
63f5691
Request background task before disconnecting
tmolitor-stud-tu Dec 29, 2024
be14aa0
Ping mucs on open to make sure we are still joined
tmolitor-stud-tu Dec 29, 2024
cf9544a
Bump all rust dependencies
tmolitor-stud-tu Dec 31, 2024
5692f8c
Bump cocoapods
tmolitor-stud-tu Dec 31, 2024
38011e5
Make sure to not crash in udp logger
tmolitor-stud-tu Jan 3, 2025
4cfce1e
Fix appex reporting in crash reports
tmolitor-stud-tu Jan 3, 2025
f20e28f
Make gateway detection more reliable when generating a room address
lissine0 Jan 3, 2025
a3c09e4
Update the copyright notice to 2025
lissine0 Jan 2, 2025
318162b
Add new action to block subscription requests, fixes #1320
tmolitor-stud-tu Jan 4, 2025
5a0f76a
Improve UDP logging while logger queue is suspended
tmolitor-stud-tu Dec 31, 2024
6fda990
Fix ios 14 compatibility
tmolitor-stud-tu Jan 4, 2025
1df1685
Revert "Use christmas logo"
tmolitor-stud-tu Dec 31, 2024
e73cfe1
Don't pile up already fired ping request delaying timers
tmolitor-stud-tu Dec 24, 2024
214a942
Add more logging to xmpp.m
tmolitor-stud-tu Dec 24, 2024
1735d39
Fix weird apple foo creating a second (unusable) framer
tmolitor-stud-tu Dec 24, 2024
d0d3c90
Fix KVOObserver usage in ContactEntry
tmolitor-stud-tu Dec 27, 2024
b494a6f
Fix crash on LMC replace without id (spec violation!)
tmolitor-stud-tu Dec 31, 2024
7443af1
Send out log messages via UDP even if global logging queue is suspended
tmolitor-stud-tu Dec 29, 2024
caf0279
Fix weird double shutdown bug
tmolitor-stud-tu Dec 29, 2024
fb037e6
Request background task before disconnecting
tmolitor-stud-tu Dec 29, 2024
adc278e
Ping mucs on open to make sure we are still joined
tmolitor-stud-tu Dec 29, 2024
d2e8308
Bump all rust dependencies
tmolitor-stud-tu Dec 31, 2024
2404dab
Bump cocoapods
tmolitor-stud-tu Dec 31, 2024
6e87d59
Make sure to not crash in udp logger
tmolitor-stud-tu Jan 3, 2025
24b8c55
Fix appex reporting in crash reports
tmolitor-stud-tu Jan 3, 2025
5507708
Make gateway detection more reliable when generating a room address
lissine0 Jan 3, 2025
0a7c23f
Update the copyright notice to 2025
lissine0 Jan 2, 2025
733f7d5
Add new action to block subscription requests, fixes #1320
tmolitor-stud-tu Jan 4, 2025
de58c0d
Improve UDP logging while logger queue is suspended
tmolitor-stud-tu Dec 31, 2024
a47d13e
Fix ios 14 compatibility
tmolitor-stud-tu Jan 4, 2025
04186f6
6.4.9-rc1 (#1359)
tmolitor-stud-tu Jan 4, 2025
d7550ff
Fix backported code
tmolitor-stud-tu Jan 4, 2025
7d83e64
6.4.9-rc1 (#1361)
tmolitor-stud-tu Jan 4, 2025
d59c3dd
Let stable pr creation workflow properly trigger our semver test
tmolitor-stud-tu Jan 4, 2025
e23fe84
Don't flush ddlog when flushing stdout or stderr stream redirector
tmolitor-stud-tu Jan 4, 2025
4851c91
Don't lock up app on close on macos
tmolitor-stud-tu Jan 4, 2025
b834631
6.4.9-rc2 (#1364)
tmolitor-stud-tu Jan 8, 2025
ced71b5
Fix termination logging
tmolitor-stud-tu Jan 8, 2025
47261ac
6.4.9-rc2 (#1365)
tmolitor-stud-tu Jan 8, 2025
593140f
Fix crash on bootup termination
tmolitor-stud-tu Jan 9, 2025
d39c644
6.4.9-rc3 (#1368)
tmolitor-stud-tu Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/create-stable-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ jobs:
body: `${{ steps.get_commits.outputs.description }}`,
});
console.log(`Created pull request #${pullRequest.data.number}`);
//update pr after creation to trigger our pr-semver-title workflow
pullRequest = await github.rest.pulls.update({
owner,
repo,
pull_number: pullRequest.data.number,
title: `${{ steps.get_commits.outputs.buildVersion }}`,
body: `${{ steps.get_commits.outputs.description }}`,
});
console.log(`Updated pull request #${pullRequest.data.number}`);
}
return pullRequest.data.number;
- name: Add Label to Pull Request
Expand Down
5 changes: 3 additions & 2 deletions Monal/Classes/ContactEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
//

struct ContactEntry<AdditionalContent: View>: View {
let contact: ObservableKVOWrapper<MLContact>
let selfnotesPrefix: Bool
let fallback: String?
@ViewBuilder let additionalContent: () -> AdditionalContent

@StateObject var contact: ObservableKVOWrapper<MLContact>

init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool = true, fallback: String? = nil) where AdditionalContent == EmptyView {
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:fallback, additionalContent:{ EmptyView() })
}
Expand All @@ -33,7 +34,7 @@ struct ContactEntry<AdditionalContent: View>: View {
}

init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.contact = contact
_contact = StateObject(wrappedValue: contact)
self.selfnotesPrefix = selfnotesPrefix
self.fallback = fallback
self.additionalContent = additionalContent
Expand Down
11 changes: 11 additions & 0 deletions Monal/Classes/ContactRequestsMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ struct ContactRequestsMenuEntry: View {
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())

Button {
// deny request
MLXMPPManager.sharedInstance().remove(contact)
MLXMPPManager.sharedInstance().block(true, contact:contact)
} label: {
Image(systemName: "xmark.circle")
.accentColor(.red)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())

Button {
// deny request
MLXMPPManager.sharedInstance().remove(contact)
Expand Down
6 changes: 6 additions & 0 deletions Monal/Classes/HelperTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ void swizzle(Class c, SEL orig, SEL new);
-(id) initWithObj:(id) obj;
@end

@interface DDLogMessage(TaggedMessage)
@property (nonatomic) BOOL ml_isDirect;
@end

@interface HelperTools : NSObject

@property (class, nonatomic, strong, nullable) DDFileLogger* fileLogger;
Expand All @@ -78,8 +82,10 @@ void swizzle(Class c, SEL orig, SEL new);
+(void) installExceptionHandler;
+(int) pendingCrashreportCount;
+(void) flushLogsWithTimeout:(double) timeout;
+(BOOL) isAppSuspended;
+(void) signalSuspension;
+(void) signalResumption;
+(void) activateTerminationLogging;
+(void) __attribute__((noreturn)) MLAssertWithText:(NSString*) text andUserData:(id _Nullable) additionalData andFile:(const char* const) file andLine:(int) line andFunc:(const char* const) func;
+(void) __attribute__((noreturn)) handleRustPanicWithText:(NSString*) text andBacktrace:(NSString*) backtrace;
+(void) __attribute__((noreturn)) throwExceptionWithName:(NSString*) name reason:(NSString*) reason userInfo:(NSDictionary* _Nullable) userInfo;
Expand Down
120 changes: 91 additions & 29 deletions Monal/Classes/HelperTools.m
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ -(void) swizzled_queueLogMessage:(DDLogMessage*) logMessage asynchronously:(BOOL
#pragma pack()


void exitLogging(void)
{
DDLogInfo(@"exit() was called...");
//make sure to unfreeze logging before flushing everything and terminating
[HelperTools activateTerminationLogging];
[HelperTools flushLogsWithTimeout:0.025];
return;
}

// see: https://developer.apple.com/library/archive/qa/qa1361/_index.html
// Returns true if the current process is being debugged (either
// running under the debugger or has a debugger attached post facto).
Expand Down Expand Up @@ -302,14 +311,45 @@ -(id) initWithObj:(id) obj
}
@end

@implementation DDLogMessage(TaggedMessage)
@dynamic ml_isDirect;
-(void) setMl_isDirect:(BOOL) value
{
objc_setAssociatedObject(self, @selector(ml_isDirect), @(value), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(BOOL) ml_isDirect
{
return ((NSNumber*)objc_getAssociatedObject(self, @selector(ml_isDirect))).boolValue;
}
@end

@implementation DDLog (AllowQueueFreeze)

-(void) swizzled_queueLogMessage:(DDLogMessage*) logMessage asynchronously:(BOOL) asyncFlag
{
//don't do sync logging for any message (usually ERROR), while the global logging queue is suspended
@synchronized(_suspensionHandling_lock) {
return [self swizzled_queueLogMessage:logMessage asynchronously:_suspensionHandling_isSuspended ? YES : asyncFlag];
//make sure this method remains performant even when checking for udp logging presence
static BOOL udpLoggerEnabled = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
udpLoggerEnabled = [[HelperTools defaultsDB] boolForKey:@"udpLoggerEnabled"];
});

//use udp logger to log all messages, even if the loggging queue is in suspended state
//this hopefully enables us to catch strange bugs sometimes hanging and then watchdog-killing the app when resuming from resumption
if(udpLoggerEnabled && _suspensionHandling_isSuspended)
{
//this marks a message as already directly logged to allow the udp logger to later ignore the queued log request for the same message
logMessage.ml_isDirect = YES;
//make sure all udp log messages are still logged chronologically and prevent race conditions with our static counter var
dispatch_async([MLUDPLogger getCurrentInstance].loggerQueue, ^{
[MLUDPLogger directlyWriteLogMessage:logMessage];
});
}

//don't do sync logging for any message (usually ERROR), while the global logging queue is suspended
//don't use _suspensionHandling_lock here because that can introduce deadlocks
//(for example if we have log statements in our MLLogFileManager code rotating the logfile and creating a new one)
return [self swizzled_queueLogMessage:logMessage asynchronously:_suspensionHandling_isSuspended ? YES : asyncFlag];
}

//see https://stackoverflow.com/a/13326633 and https://fek.io/blog/method-swizzling-in-obj-c-and-swift/
Expand Down Expand Up @@ -391,10 +431,8 @@ +(void) __attribute__((noreturn)) handleRustPanicWithText:(NSString*) text andBa
_crash_info.backtrace = backtrace.UTF8String;

//log error and flush all logs
[DDLog flushLog];
DDLogError(@"*****************\n%@\n%@", abort_msg, backtrace);
[DDLog flushLog];
[HelperTools flushLogsWithTimeout:0.250];
[HelperTools flushLogsWithTimeout:0.025];

//now abort everything
abort();
Expand Down Expand Up @@ -1867,32 +1905,37 @@ +(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage coun

//construct json dictionary
(*counter)++;
NSDictionary* representedObject = @{
@"queueThreadLabel": [self getQueueThreadLabelFor:logMessage],
NSDictionary* tag = @{
@"queueThreadLabel": nilWrapper([self getQueueThreadLabelFor:logMessage]),
@"processType": [self isAppExtension] ? @"appex" : @"mainapp",
@"processName": [[[NSBundle mainBundle] executablePath] lastPathComponent],
@"counter": [NSNumber numberWithUnsignedLongLong:*counter],
@"processID": _processID,
@"qosName": qos2name(logMessage.qos),
@"representedObject": logMessage.representedObject ? logMessage.representedObject : [NSNull null],
@"processName": nilWrapper([[[NSBundle mainBundle] executablePath] lastPathComponent]),
@"counter": @(*counter),
@"processID": nilWrapper(_processID),
@"qosName": nilWrapper(qos2name(logMessage.qos)),
@"loggingQueueSuspended": @(_suspensionHandling_isSuspended),
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@"tag": nilWrapper(logMessage.tag),
#pragma clang diagnostic pop
};
NSDictionary* msgDict = @{
@"messageFormat": logMessage.messageFormat,
@"message": logMessage.message,
@"level": [NSNumber numberWithInteger:logMessage.level],
@"flag": [NSNumber numberWithInteger:logMessage.flag],
@"context": [NSNumber numberWithInteger:logMessage.context],
@"file": logMessage.file,
@"fileName": logMessage.fileName,
@"function": logMessage.function,
@"line": [NSNumber numberWithInteger:logMessage.line],
@"tag": representedObject,
@"options": [NSNumber numberWithInteger:logMessage.options],
@"messageFormat": nilWrapper(logMessage.messageFormat),
@"message": nilWrapper(logMessage.message),
@"level": @(logMessage.level),
@"flag": @(logMessage.flag),
@"context": @(logMessage.context),
@"file": nilWrapper(logMessage.file),
@"fileName": nilWrapper(logMessage.fileName),
@"function": nilWrapper(logMessage.function),
@"line": @(logMessage.line),
@"representedObject": nilWrapper(logMessage.representedObject),
@"tag": nilWrapper(tag),
@"options": @(logMessage.options),
@"timestamp": [dateFormatter stringFromDate:logMessage.timestamp],
@"threadID": logMessage.threadID,
@"threadName": logMessage.threadName,
@"queueLabel": logMessage.queueLabel,
@"qos": [NSNumber numberWithInteger:logMessage.qos],
@"threadID": nilWrapper(logMessage.threadID),
@"threadName": nilWrapper(logMessage.threadName),
@"queueLabel": nilWrapper(logMessage.queueLabel),
@"qos": @(logMessage.qos),
};

//encode json into NSData
Expand All @@ -1915,6 +1958,13 @@ +(void) flushLogsWithTimeout:(double) timeout
[MLUDPLogger flushWithTimeout:timeout];
}

+(BOOL) isAppSuspended
{
@synchronized(_suspensionHandling_lock) {
return _suspensionHandling_isSuspended;
}
}

+(void) signalSuspension
{
@synchronized(_suspensionHandling_lock) {
Expand Down Expand Up @@ -1946,6 +1996,18 @@ +(void) signalResumption
}
}

+(void) activateTerminationLogging
{
@synchronized(_suspensionHandling_lock) {
if(_suspensionHandling_isSuspended)
{
DDLogVerbose(@"Activating logging for app termination...");
dispatch_resume([DDLog loggingQueue]);
_suspensionHandling_isSuspended = NO;
}
}
}

+(void) configureXcodeLogging
{
//only start console logger
Expand Down Expand Up @@ -2074,7 +2136,7 @@ +(void) installCrashHandler
handler.maxReportCount = 4;
handler.deadlockWatchdogInterval = 0; // no main thread watchdog
handler.userInfo = @{
@"isAppex": @([self isAppExtension]),
@"isAppex": bool2str([self isAppExtension]),
@"processName": [[[NSBundle mainBundle] executablePath] lastPathComponent],
@"bundleName": nilWrapper([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]),
@"appVersion": [self appBuildVersionInfoFor:MLVersionTypeLog],
Expand Down
25 changes: 15 additions & 10 deletions Monal/Classes/MLMessageProcessor.m
Original file line number Diff line number Diff line change
Expand Up @@ -579,18 +579,23 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag
{
NSString* messageIdToReplace = [messageNode findFirst:@"{urn:xmpp:message-correct:0}replace@id"];
DDLogVerbose(@"Message id to LMC-replace: %@", messageIdToReplace);
//this checks if this message is from the same jid as the message it tries to do the LMC for (e.g. inbound can only correct inbound and outbound only outbound)
historyId = [[DataLayer sharedInstance] getLMCHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser occupantId:occupantId participantJid:participantJid andAccount:account.accountNo];
DDLogVerbose(@"History id to LMC-replace: %@", historyId);
//now check if the LMC is allowed (we use historyIdToUse for MLhistory mam queries to only check LMC for the 3 messages coming before this ID in this converastion)
//historyIdToUse will be nil, for messages going forward in time which means (check for the newest 3 messages in this conversation)
if(historyId != nil && [[DataLayer sharedInstance] checkLMCEligible:historyId encrypted:encrypted historyBaseID:historyIdToUse])
if(messageIdToReplace == nil)
DDLogWarn(@"Ignoring LMC message not carrying a replacement id, spec vialoation!");
else
{
[[DataLayer sharedInstance] updateMessageHistory:historyId withText:body];
LMCReplaced = YES;
//this checks if this message is from the same jid as the message it tries to do the LMC for (e.g. inbound can only correct inbound and outbound only outbound)
historyId = [[DataLayer sharedInstance] getLMCHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser occupantId:occupantId participantJid:participantJid andAccount:account.accountNo];
DDLogVerbose(@"History id to LMC-replace: %@", historyId);
//now check if the LMC is allowed (we use historyIdToUse for MLhistory mam queries to only check LMC for the 3 messages coming before this ID in this converastion)
//historyIdToUse will be nil, for messages going forward in time which means (check for the newest 3 messages in this conversation)
if(historyId != nil && [[DataLayer sharedInstance] checkLMCEligible:historyId encrypted:encrypted historyBaseID:historyIdToUse])
{
[[DataLayer sharedInstance] updateMessageHistory:historyId withText:body];
LMCReplaced = YES;
}
else
historyId = nil;
}
else
historyId = nil;
}

//handle normal messages or LMC messages that can not be found
Expand Down
5 changes: 4 additions & 1 deletion Monal/Classes/MLMucProcessor.m
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,10 @@ -(NSString* _Nullable) generateMucJid
NSString* mucServer = nil;
for(NSString* jid in _account.connectionProperties.conferenceServers)
{
if([_account.connectionProperties.conferenceServers[jid] check:@"identity<type=text>"])
// Do not use gateways
if(![_account.connectionProperties.conferenceServers[jid] check:@"identity<category=gateway>"]
&& [_account.connectionProperties.conferenceServers[jid] check:@"identity<category=conference>"]
&& [_account.connectionProperties.conferenceServers[jid] check:@"identity<type=text>"])
{
mucServer = jid;
break;
Expand Down
65 changes: 27 additions & 38 deletions Monal/Classes/MLStream.m
Original file line number Diff line number Diff line change
Expand Up @@ -470,59 +470,48 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host
return YES;
});

/*
//some weird apple stuff creates the framer twice: once directly when starting the tcp handshake
//and again later after the tcp connection was established successfully --> ignore the first one
//and once a few milliseconds later, presumably after the tcp connection was established successfully
//--> ignore all but the first one
if(framerId < 1)
{
nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) {
nw_framer_parse_input(framer, 1, BUFFER_SIZE, nil, ^size_t(uint8_t* buffer, size_t buffer_length, bool is_complete) {
MLAssert(NO, @"Unexpected incoming bytes in first framer!", (@{
@"logtag": nilWrapper(logtag),
@"framer": framer,
@"buffer": [NSData dataWithBytes:buffer length:buffer_length],
@"buffer_length": @(buffer_length),
@"is_complete": bool2str(is_complete),
}));
return buffer_length;
DDLogVerbose(@"Framer is the first one, using it...");
//we have to simulate nw_connection_state_ready because the connection state will not reflect that while our framer is active
//--> use framer start as "connection active" signal
//first framer start is allowed to directly send data which will be used as tcp early data
if(!wasOpenOnce)
{
wasOpenOnce = YES;
@synchronized(shared_state) {
shared_state.open = YES;
}
//make sure to not do this inside the framer thread to not cause any deadlocks
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[input generateEvent:NSStreamEventOpenCompleted];
[output generateEvent:NSStreamEventOpenCompleted];
});
return 0; //why that?
}

nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) {
DDLogDebug(@"Got new input for framer: %@", framer);
[input schedule_read];
return 0; //why that??
});
nw_framer_set_output_handler(framer, ^(nw_framer_t framer, nw_framer_message_t message, size_t message_length, bool is_complete) {
MLAssert(NO, @"Unexpected outgoing bytes in first framer!", (@{
MLAssert(NO, @"Unexpected outgoing bytes in framer!", (@{
@"logtag": nilWrapper(logtag),
@"framer": framer,
@"message": message,
@"message_length": @(message_length),
@"is_complete": bool2str(is_complete),
}));
});
return nw_framer_start_result_will_mark_ready;
}
*/

//we have to simulate nw_connection_state_ready because the connection state will not reflect that while our framer is active
//--> use framer start as "connection active" signal
//first framer start is allowed to directly send data which will be used as tcp early data
if(!wasOpenOnce)
{
wasOpenOnce = YES;
@synchronized(shared_state) {
shared_state.open = YES;
}
//make sure to not do this inside the framer thread to not cause any deadlocks
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[input generateEvent:NSStreamEventOpenCompleted];
[output generateEvent:NSStreamEventOpenCompleted];
});

shared_state.framer = framer;
}
else
DDLogVerbose(@"Ignoring subsequent framer...");

nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) {
[input schedule_read];
return 0; //why that??
});

shared_state.framer = framer;
return nw_framer_start_result_will_mark_ready;
});
DDLogInfo(@"%@: Not doing direct TLS: appending framer to protocol stack...", logtag);
Expand Down
Loading
Loading