Merge pull request #57 from loki-project/custom-server

Custom Public Chats
pull/59/head
gmbnt 6 years ago committed by GitHub
commit dcca0ccb66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1 +1 @@
Subproject commit 56980ceea7cc0964b92f98831e79fea03e76e801
Subproject commit 1bec50723dafad9d8932e13d36d4a215341f5ab0

@ -11,6 +11,7 @@
241C6315231F64CE00B4198E /* CGFloat+Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */; };
241C6316231F64CE00B4198E /* UIColor+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6310231F5C4400B4198E /* UIColor+Helper.swift */; };
24A830A22293CD0100F4CAC0 /* LokiP2PServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */; };
24BD2609234DA2050008EB0A /* NewPublicChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */; };
2AE2882E4C2B96BFFF9EE27C /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */; };
3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; };
34074F61203D0CBE004596AE /* OWSSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = 34074F5F203D0CBD004596AE /* OWSSounds.m */; };
@ -564,8 +565,7 @@
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821F2F72272CED3002C88C0 /* DisplayNameVC.swift */; };
B821F2FA2272CEEE002C88C0 /* SeedVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821F2F92272CEEE002C88C0 /* SeedVC.swift */; };
B8258493230FA5E9001B41CB /* ScanQRCodeVC.m in Sources */ = {isa = PBXBuildFile; fileRef = B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */; };
B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* RSSFeedPoller.swift */; };
B845B4D4230CD09100D759F0 /* GroupChatPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B845B4D3230CD09000D759F0 /* GroupChatPoller.swift */; };
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */; };
B846365B22B7418B00AF1514 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */; };
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; };
@ -682,6 +682,7 @@
241C6310231F5C4400B4198E /* UIColor+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helper.swift"; sourceTree = "<group>"; };
241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Rounding.swift"; sourceTree = "<group>"; };
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiP2PServer.swift; sourceTree = "<group>"; };
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPublicChatVC.swift; sourceTree = "<group>"; };
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareButtonsView.m; sourceTree = "<group>"; };
3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareButtonsView.h; sourceTree = "<group>"; };
@ -1376,8 +1377,7 @@
B821F2F92272CEEE002C88C0 /* SeedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVC.swift; sourceTree = "<group>"; };
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScanQRCodeVC.h; sourceTree = "<group>"; };
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScanQRCodeVC.m; sourceTree = "<group>"; };
B825849F2315024B001B41CB /* RSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSFeedPoller.swift; sourceTree = "<group>"; };
B845B4D3230CD09000D759F0 /* GroupChatPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatPoller.swift; sourceTree = "<group>"; };
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiRSSFeedPoller.swift; sourceTree = "<group>"; };
B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identicon+ObjC.swift"; sourceTree = "<group>"; };
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
B86BD08023399883000F5AE3 /* QRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeModal.swift; sourceTree = "<group>"; };
@ -2647,18 +2647,10 @@
B8439518228510E9000563FE /* Loki */ = {
isa = PBXGroup;
children = (
B86BD0892339A278000F5AE3 /* Group Chat */,
B8BFFF392355426100102A27 /* Messaging */,
B86BD0872339A1ED000F5AE3 /* Onboarding */,
B86BD08223399ABF000F5AE3 /* Settings */,
B86BD0882339A253000F5AE3 /* Utilities */,
B8162F0222891AD600D46544 /* FriendRequestView.swift */,
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */,
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */,
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
);
path = Loki;
sourceTree = "<group>";
@ -2679,9 +2671,9 @@
children = (
B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */,
B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
B86BD08023399883000F5AE3 /* QRCodeModal.swift */,
B86BD08523399CEF000F5AE3 /* SeedModal.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
);
path = Settings;
sourceTree = "<group>";
@ -2698,20 +2690,28 @@
B86BD0882339A253000F5AE3 /* Utilities */ = {
isa = PBXGroup;
children = (
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
B86BD08323399ACF000F5AE3 /* Modal.swift */,
B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */,
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
B86BD0892339A278000F5AE3 /* Group Chat */ = {
B8BFFF392355426100102A27 /* Messaging */ = {
isa = PBXGroup;
children = (
B845B4D3230CD09000D759F0 /* GroupChatPoller.swift */,
B825849F2315024B001B41CB /* RSSFeedPoller.swift */,
B8162F0222891AD600D46544 /* FriendRequestView.swift */,
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */,
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */,
B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */,
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */,
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
);
path = "Group Chat";
path = Messaging;
sourceTree = "<group>";
};
D221A07E169C9E5E00537ABF = {
@ -3753,7 +3753,6 @@
34A4C62022175C5C0042EF2E /* OnboardingProfileViewController.swift in Sources */,
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */,
EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */,
B845B4D4230CD09100D759F0 /* GroupChatPoller.swift in Sources */,
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */,
4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */,
@ -3787,7 +3786,7 @@
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */,
B82584A02315024B001B41CB /* LokiRSSFeedPoller.swift in Sources */,
24A830A22293CD0100F4CAC0 /* LokiP2PServer.swift in Sources */,
349ED990221B0194008045B0 /* Onboarding2FAViewController.swift in Sources */,
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
@ -3855,6 +3854,7 @@
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */,
B885D5F62334A32100EE0D8E /* UIView+Constraint.swift in Sources */,
24BD2609234DA2050008EB0A /* NewPublicChatVC.swift in Sources */,
34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */,
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */,
34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */,

@ -10,9 +10,8 @@ extern NSString *const AppDelegateStoryboardMain;
- (void)startLongPollerIfNeeded;
- (void)stopLongPollerIfNeeded;
- (void)createGroupChatsIfNeeded;
- (void)setUpDefaultPublicChatsIfNeeded;
- (void)createRSSFeedsIfNeeded;
- (void)startGroupChatPollersIfNeeded;
- (void)startRSSFeedPollersIfNeeded;
@end

@ -69,7 +69,6 @@ static NSTimeInterval launchStartedAt;
// Loki
@property (nonatomic) LKP2PServer *lokiP2PServer;
@property (nonatomic) LKLongPoller *lokiLongPoller;
@property (nonatomic) LKGroupChatPoller *lokiPublicChatPoller;
@property (nonatomic) LKRSSFeedPoller *lokiNewsFeedPoller;
@property (nonatomic) LKRSSFeedPoller *lokiMessengerUpdatesFeedPoller;
@ -1525,11 +1524,6 @@ static NSTimeInterval launchStartedAt;
[self.lokiLongPoller stopIfNeeded];
}
- (LKGroupChat *)lokiPublicChat
{
return [[LKGroupChat alloc] initWithServerID:LKGroupChatAPI.publicChatServerID server:LKGroupChatAPI.publicChatServer displayName:NSLocalizedString(@"Loki Public Chat", @"") isDeletable:true];
}
- (LKRSSFeed *)lokiNewsFeed
{
return [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:NSLocalizedString(@"Loki News", @"") isDeletable:true];
@ -1540,27 +1534,19 @@ static NSTimeInterval launchStartedAt;
return [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:NSLocalizedString(@"Loki Messenger Updates", @"") isDeletable:false];
}
- (void)createGroupChatsIfNeeded
- (void)setUpDefaultPublicChatsIfNeeded
{
LKGroupChat *publicChat = self.lokiPublicChat;
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *userDefaultsKey = [@"isGroupChatSetUp." stringByAppendingString:publicChat.id];
BOOL isChatSetUp = [NSUserDefaults.standardUserDefaults boolForKey:userDefaultsKey];
if (!isChatSetUp || !publicChat.isDeletable) {
TSGroupModel *group = [[TSGroupModel alloc] initWithTitle:publicChat.displayName memberIds:@[ userHexEncodedPublicKey, publicChat.server ] image:nil groupId:[publicChat.id dataUsingEncoding:NSUTF8StringEncoding]];
__block TSGroupThread *thread;
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [TSGroupThread getOrCreateThreadWithGroupModel:group transaction:transaction];
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
NSCalendar *calendar = NSCalendar.currentCalendar;
[calendar setTimeZone:timeZone];
NSDateComponents *dateComponents = [NSDateComponents new];
[dateComponents setYear:999];
NSDate *date = [calendar dateByAddingComponents:dateComponents toDate:[NSDate new] options:0];
[thread updateWithMutedUntilDate:date transaction:transaction];
}];
[OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:userDefaultsKey];
for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) {
NSString *userDefaultsKey = [@"isGroupChatSetUp." stringByAppendingString:chat.id]; // Should ideally be isPublicChatSetUp
BOOL isChatSetUp = [NSUserDefaults.standardUserDefaults boolForKey:userDefaultsKey];
if (!isChatSetUp || !chat.isDeletable) {
[LKPublicChatManager.shared addChatWithServer:chat.server channel:chat.channel name:chat.displayName];
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:chat.idAsData transaction:transaction];
if (thread != nil) { [OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread]; }
}];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:userDefaultsKey];
}
}
}
@ -1590,18 +1576,6 @@ static NSTimeInterval launchStartedAt;
}
}
- (void)createGroupChatPollersIfNeeded
{
// Only create the group chat pollers if their threads aren't deleted
__block TSGroupThread *thread;
[OWSPrimaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
thread = [TSGroupThread threadWithGroupId:[self.lokiPublicChat.id dataUsingEncoding:NSUTF8StringEncoding] transaction:transaction];
}];
if (thread != nil && self.lokiPublicChatPoller == nil) {
self.lokiPublicChatPoller = [[LKGroupChatPoller alloc] initForGroup:self.lokiPublicChat];
}
}
- (void)createRSSFeedPollersIfNeeded
{
// Only create the RSS feed pollers if their threads aren't deleted
@ -1618,12 +1592,6 @@ static NSTimeInterval launchStartedAt;
}
}
- (void)startGroupChatPollersIfNeeded
{
[self createGroupChatPollersIfNeeded];
if (self.lokiPublicChatPoller != nil) { [self.lokiPublicChatPoller startIfNeeded]; }
}
- (void)startRSSFeedPollersIfNeeded
{
[self createRSSFeedPollersIfNeeded];
@ -1635,10 +1603,6 @@ static NSTimeInterval launchStartedAt;
NSDictionary *userInfo = notification.userInfo;
NSString *threadID = (NSString *)userInfo[@"threadId"];
if (threadID == nil) { return; }
if ([threadID isEqualToString:[TSGroupThread threadIdFromGroupId:[self.lokiPublicChat.id dataUsingEncoding:NSUTF8StringEncoding]]] && self.lokiPublicChatPoller != nil) {
[self.lokiPublicChatPoller stop];
self.lokiPublicChatPoller = nil;
}
if ([threadID isEqualToString:[TSGroupThread threadIdFromGroupId:[self.lokiNewsFeed.id dataUsingEncoding:NSUTF8StringEncoding]]] && self.lokiNewsFeedPoller != nil) {
[self.lokiNewsFeedPoller stop];
self.lokiNewsFeedPoller = nil;

@ -1,7 +1,7 @@
import FeedKit
@objc(LKRSSFeedPoller)
public final class RSSFeedPoller : NSObject {
public final class LokiRSSFeedPoller : NSObject {
private let feed: LokiRSSFeed
private var timer: Timer? = nil
private var hasStarted = false

@ -4,9 +4,16 @@
@objc(LKMentionCandidateSelectionView)
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
@objc var hasGroupContext = false
@objc var publicChatServer: String?
var publicChatChannel: UInt64?
@objc var delegate: MentionCandidateSelectionViewDelegate?
// MARK: Convenience
@objc(setPublicChatChannel:)
func setPublicChatChannel(to publicChatChannel: UInt64) {
self.publicChatChannel = publicChatChannel != 0 ? publicChatChannel : nil
}
// MARK: Components
@objc lazy var tableView: UITableView = { // TODO: Make this private
let result = UITableView()
@ -44,7 +51,8 @@ final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITab
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
let mentionCandidate = mentionCandidates[indexPath.row]
cell.mentionCandidate = mentionCandidate
cell.hasGroupContext = hasGroupContext
cell.publicChatServer = publicChatServer
cell.publicChatChannel = publicChatChannel
return cell
}
@ -61,7 +69,8 @@ private extension MentionCandidateSelectionView {
final class Cell : UITableViewCell {
var mentionCandidate = Mention(hexEncodedPublicKey: "", displayName: "") { didSet { update() } }
var hasGroupContext = false
var publicChatServer: String?
var publicChatChannel: UInt64?
// MARK: Components
private lazy var profilePictureImageView = AvatarImageView()
@ -121,8 +130,12 @@ private extension MentionCandidateSelectionView {
displayNameLabel.text = mentionCandidate.displayName
let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
profilePictureImageView.image = profilePicture
let isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: LokiGroupChatAPI.publicChatServerID, on: LokiGroupChatAPI.publicChatServer)
moderatorIconImageView.isHidden = !isUserModerator || !hasGroupContext
if let server = publicChatServer, let channel = publicChatChannel {
let isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: channel, on: server)
moderatorIconImageView.isHidden = !isUserModerator
} else {
moderatorIconImageView.isHidden = true
}
}
}
}

@ -6,11 +6,11 @@ final class NewConversationVC : OWSViewController, OWSQRScannerDelegate {
private lazy var publicKeyTextField: UITextField = {
let result = UITextField()
result.textColor = Theme.primaryColor
result.font = UIFont.ows_dynamicTypeBodyClamped
result.font = .ows_dynamicTypeBodyClamped
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter a Public Key", comment: ""))
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
result.attributedPlaceholder = placeholder
result.tintColor = UIColor.lokiGreen()
result.tintColor = .lokiGreen()
result.keyboardAppearance = .dark
return result
}()

@ -0,0 +1,100 @@
@objc(LKNewPublicChatVC)
final class NewPublicChatVC : OWSViewController {
// MARK: Components
private lazy var urlTextField: UITextField = {
let result = UITextField()
result.textColor = Theme.primaryColor
result.font = .ows_dynamicTypeBodyClamped
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter a URL", comment: ""))
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
result.attributedPlaceholder = placeholder
result.tintColor = .lokiGreen()
result.keyboardAppearance = .dark
result.keyboardType = .URL
result.autocapitalizationType = .none
return result
}()
private lazy var addButton = OWSFlatButton.button(title: NSLocalizedString("Add", comment: ""), font: UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight(), titleColor: .white, backgroundColor: .lokiGreen(), target: self, selector: #selector(handleAddButtonTapped))
// MARK: Lifecycle
override func viewDidLoad() {
// Background color & margins
view.backgroundColor = Theme.backgroundColor
view.layoutMargins = .zero
// Navigation bar
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close))
title = NSLocalizedString("Add Public Chat", comment: "")
// Separator
let separator = UIView()
separator.autoSetDimension(.height, toSize: 1 / UIScreen.main.scale)
separator.backgroundColor = Theme.hairlineColor
// Explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Theme.primaryColor
explanationLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped
explanationLabel.text = NSLocalizedString("Enter the URL of the public chat you'd like to join. The Loki Public Chat URL is https://chat.lokinet.org.", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
// Add button
let addButtonHeight = addButton.button.titleLabel!.font.pointSize * 48 / 17
addButton.autoSetDimension(.height, toSize: addButtonHeight)
updateAddButton(isConnecting: false)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ urlTextField, UIView.spacer(withHeight: 8), separator, UIView.spacer(withHeight: 24), explanationLabel, UIView.vStretchingSpacer(), addButton ])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.autoPinWidthToSuperview()
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
urlTextField.becomeFirstResponder()
}
// MARK: Updating
private func updateAddButton(isConnecting: Bool) {
addButton.setEnabled(!isConnecting)
addButton.setTitle(isConnecting ? NSLocalizedString("Connecting...", comment: "") : NSLocalizedString("Add", comment: ""))
}
// MARK: General
private func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
// MARK: Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
@objc private func handleAddButtonTapped() {
let uncheckedURL = (urlTextField.text?.trimmingCharacters(in: .whitespaces) ?? "").lowercased().replacingOccurrences(of: "http://", with: "https://")
guard let url = URL(string: uncheckedURL), let scheme = url.scheme, scheme == "https", url.host != nil else {
return showError(title: NSLocalizedString("Invalid URL", comment: ""), message: NSLocalizedString("Please check the URL you entered and try again.", comment: ""))
}
updateAddButton(isConnecting: true)
let channelID: UInt64 = 1
let urlAsString = url.absoluteString
let displayName = OWSProfileManager.shared().localProfileName()
LokiPublicChatManager.shared.addChat(server: urlAsString, channel: channelID)
.done(on: .main) { [weak self] _ in
let _ = LokiPublicChatAPI.getMessages(for: channelID, on: urlAsString)
let _ = LokiPublicChatAPI.setDisplayName(to: displayName, on: urlAsString)
self?.presentingViewController!.dismiss(animated: true, completion: nil)
}
.catch(on: .main) { [weak self] _ in
self?.updateAddButton(isConnecting: false)
self?.showError(title: NSLocalizedString("Couldn't Connect", comment: ""))
}
}
}

@ -4,12 +4,12 @@ final class DisplayNameVC : OnboardingBaseViewController {
private lazy var userNameTextField: UITextField = {
let result = UITextField()
result.textColor = Theme.primaryColor
result.font = UIFont.ows_dynamicTypeBodyClamped
result.font = .ows_dynamicTypeBodyClamped
result.textAlignment = .center
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Display Name", comment: ""))
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
result.attributedPlaceholder = placeholder
result.tintColor = UIColor.lokiGreen()
result.tintColor = .lokiGreen()
result.accessibilityIdentifier = "onboarding.accountDetailsStep.userNameTextField"
result.keyboardAppearance = .dark
return result
@ -64,6 +64,11 @@ final class DisplayNameVC : OnboardingBaseViewController {
TSAccountManager.sharedInstance().didRegister()
UserDefaults.standard.set(true, forKey: "didUpdateForMainnet")
onboardingController.verificationDidComplete(fromView: self)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.setUpDefaultPublicChatsIfNeeded()
appDelegate.createRSSFeedsIfNeeded()
LokiPublicChatManager.shared.startPollersIfNeeded()
appDelegate.startRSSFeedPollersIfNeeded()
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { }) // Try to save the user name but ignore the result
}
}

@ -4,17 +4,21 @@ public final class MentionUtilities : NSObject {
override private init() { }
@objc public static func highlightMentions(in string: String, thread: TSThread) -> String {
return highlightMentions(in: string, isOutgoingMessage: false, thread: thread, attributes: [:]).string // isOutgoingMessage and attributes are irrelevant
@objc public static func highlightMentions(in string: String, threadID: String) -> String {
return highlightMentions(in: string, isOutgoingMessage: false, threadID: threadID, attributes: [:]).string // isOutgoingMessage and attributes are irrelevant
}
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, thread: TSThread, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
var publicChat: LokiPublicChat?
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction)
}
var string = string
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: [])
let knownUserHexEncodedPublicKeys = LokiAPI.userHexEncodedPublicKeyCache[thread.uniqueId!] ?? [] // Should always be populated at this point
let knownUserHexEncodedPublicKeys = LokiAPI.userHexEncodedPublicKeyCache[threadID] ?? [] // Should always be populated at this point
var mentions: [NSRange] = []
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.count))
while let match = outerMatch, thread.isGroupThread() {
while let match = outerMatch {
let hexEncodedPublicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
let matchEnd: Int
if knownUserHexEncodedPublicKeys.contains(hexEncodedPublicKey) {
@ -22,9 +26,10 @@ public final class MentionUtilities : NSObject {
if hexEncodedPublicKey == OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey {
userDisplayName = OWSProfileManager.shared().localProfileName()
} else {
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
userDisplayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String?
if let publicChat = publicChat {
userDisplayName = DisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server)
} else {
userDisplayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey)
}
}
if let userDisplayName = userDisplayName {

@ -707,7 +707,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *text = displayableText.displayText;
NSMutableAttributedString *attributedText = [LKMentionUtilities highlightMentionsIn:text isOutgoingMessage:isOutgoingMessage thread:thread attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }].mutableCopy;
NSMutableAttributedString *attributedText = [LKMentionUtilities highlightMentionsIn:text isOutgoingMessage:isOutgoingMessage threadID:thread.uniqueId attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }].mutableCopy;
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
NSString *searchableText = [FullTextSearchFinder normalizeWithText:searchText];
@ -1158,7 +1158,7 @@ NS_ASSUME_NONNULL_BEGIN
if (!self.viewItem.hasQuotedText) { return nil; }
NSString *rawText = self.viewItem.displayableQuotedText.fullText;
TSThread *thread = self.viewItem.interaction.thread;
NSString *text = [LKMentionUtilities highlightMentionsIn:rawText thread:thread];
NSString *text = [LKMentionUtilities highlightMentionsIn:rawText threadID:thread.uniqueId];
return [DisplayableText displayableText:text];
}

@ -7,6 +7,7 @@
#import "OWSMessageBubbleView.h"
#import "OWSMessageHeaderView.h"
#import "Session-Swift.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -298,10 +299,16 @@ NS_ASSUME_NONNULL_BEGIN
[self.contentView addSubview:self.avatarView];
if (self.viewItem.isGroupThread && !self.viewItem.isRSSFeed) {
BOOL isModerator = [LKGroupChatAPI isUserModerator:incomingMessage.authorId forGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer];
UIImage *moderatorIcon = [UIImage imageNamed:@"Crown"];
self.moderatorIconImageView.image = moderatorIcon;
self.moderatorIconImageView.hidden = !isModerator;
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.viewItem.interaction.uniqueThreadId transaction: transaction];
}];
if (publicChat != nil) {
BOOL isModerator = [LKPublicChatAPI isUserModerator:incomingMessage.authorId forGroup:publicChat.channel onServer:publicChat.server];
UIImage *moderatorIcon = [UIImage imageNamed:@"Crown"];
self.moderatorIconImageView.image = moderatorIcon;
self.moderatorIconImageView.hidden = !isModerator;
}
}
[self.contentView addSubview:self.moderatorIconImageView];

@ -13,6 +13,7 @@
#import <SignalMessaging/UIView+OWS.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -553,9 +554,12 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3;
if (quotedAuthor == self.quotedMessage.authorId) {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSString *collection = [NSString stringWithFormat:@"%@.%@", LKGroupChatAPI.publicChatServer, @(LKGroupChatAPI.publicChatServerID)];
NSString *displayName = [transaction stringForKey:self.quotedMessage.authorId inCollection:collection];
if (displayName != nil) { quotedAuthor = displayName; }
LKPublicChat *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.quotedMessage.threadId transaction:transaction];
if (publicChat != nil) {
quotedAuthor = [LKDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:publicChat.channel on:publicChat.server using:transaction];
} else {
quotedAuthor = [LKDisplayNameUtilities getPrivateChatDisplayNameFor:self.quotedMessage.authorId];
}
}];
}

@ -1093,7 +1093,14 @@ const CGFloat kMaxTextViewHeight = 98;
- (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread
{
self.mentionCandidateSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
}];
if (publicChat != nil) {
self.mentionCandidateSelectionView.publicChatServer = publicChat.server;
[self.mentionCandidateSelectionView setPublicChatChannel:publicChat.channel];
}
self.mentionCandidateSelectionView.mentionCandidates = mentionCandidates;
self.mentionCandidateSelectionViewSizeConstraint.constant = 6 + MIN(mentionCandidates.count, 4) * 52;
[self setNeedsLayout];

@ -2609,6 +2609,7 @@ typedef enum : NSUInteger {
__block OWSQuotedReplyModel *quotedReply;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
quotedReply = [OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:conversationItem
threadId:conversationItem.interaction.uniqueThreadId
transaction:transaction];
}];

@ -620,7 +620,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
// since that logic may exit early.
if (message.quotedMessage) {
self.quotedReply =
[OWSQuotedReplyModel quotedReplyWithQuotedMessage:message.quotedMessage transaction:transaction];
[OWSQuotedReplyModel quotedReplyWithQuotedMessage:message.quotedMessage threadId:message.uniqueThreadId transaction:transaction];
if (self.quotedReply.body.length > 0) {
self.displayableQuotedText =
@ -1187,9 +1187,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
TSMessage *message = (TSMessage *)self.interaction;
if (!message.isGroupChatMessage) return;
__block LKPublicChat *publicChat;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
}];
if (publicChat == nil) return;
// Delete the message
BOOL isSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
[[LKGroupChatAPI deleteMessageWithID:message.groupChatServerID forGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer isSentByUser:isSentByUser].catch(^(NSError *error) {
[[LKPublicChatAPI deleteMessageWithID:message.groupChatServerID forGroup:publicChat.channel onServer:publicChat.server isSentByUser:isSentByUser].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
@ -1256,9 +1262,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
TSMessage *message = (TSMessage *)self.interaction;
if (!message.isGroupChatMessage) return false;
// Ensure we have the details needed to contact the server
__block LKPublicChat *publicChat;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
}];
if (publicChat == nil) return false;
// Only allow deletion on incoming messages if the user has moderation permission
if (interationType == OWSInteractionType_IncomingMessage) {
BOOL isModerator = [LKGroupChatAPI isUserModerator:self.userHexEncodedPublicKey forGroup:LKGroupChatAPI.publicChatServerID onServer: LKGroupChatAPI.publicChatServer];
BOOL isModerator = [LKPublicChatAPI isUserModerator:self.userHexEncodedPublicKey forGroup:publicChat.channel onServer:publicChat.server];
if (!isModerator) return false;
}

@ -2016,7 +2016,7 @@ NS_ASSUME_NONNULL_BEGIN
transaction:transaction
conversationStyle:conversationStyle];
quotedMessage = [
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem transaction:transaction]
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem threadId:viewItem.interaction.uniqueThreadId transaction:transaction]
buildQuotedMessageForSending];
} else {
TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread
@ -2037,8 +2037,7 @@ NS_ASSUME_NONNULL_BEGIN
isRSSFeed:NO
transaction:transaction
conversationStyle:conversationStyle];
quotedMessage = [
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem transaction:transaction]
quotedMessage = [[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem threadId:viewItem.interaction.uniqueThreadId transaction:transaction]
buildQuotedMessageForSending];
}
OWSAssertDebug(quotedMessage);

@ -396,7 +396,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *displayableText = thread.lastMessageText;
if (displayableText) {
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:thread.threadRecord.uniqueId in:nil]; // TODO: Terrible place to do this, but okay for now
displayableText = [LKMentionUtilities highlightMentionsIn:displayableText thread:thread.threadRecord];
displayableText = [LKMentionUtilities highlightMentionsIn:displayableText threadID:thread.threadRecord.uniqueId];
[snippetText appendAttributedString:[[NSAttributedString alloc]
initWithString:displayableText
attributes:@{

@ -693,9 +693,9 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
}
if (OWSIdentityManager.sharedManager.identityKeyPair != nil) {
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
[appDelegate createGroupChatsIfNeeded];
[appDelegate setUpDefaultPublicChatsIfNeeded];
[appDelegate createRSSFeedsIfNeeded];
[appDelegate startGroupChatPollersIfNeeded];
[LKPublicChatManager.shared startPollersIfNeeded];
[appDelegate startRSSFeedPollersIfNeeded];
}
}
@ -787,11 +787,15 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
self.navigationItem.leftBarButtonItem = settingsButton;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, settingsButton);
self.navigationItem.rightBarButtonItem =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose
target:self
action:@selector(showNewConversationView)
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"compose")];
UIBarButtonItem *newPrivateChatButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCompose
target:self
action:@selector(showNewConversationVC)
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"compose")];
UIBarButtonItem *newGroupChatButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"group-avatar"] style:UIBarButtonItemStylePlain target:self action:@selector(showNewPublicChatVC)];
self.navigationItem.rightBarButtonItems = @[ newPrivateChatButton, newGroupChatButton ];
}
- (void)settingsButtonPressed:(id)sender
@ -833,7 +837,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
[self.navigationController pushViewController:vc animated:NO];
}
- (void)showNewConversationView
- (void)showNewConversationVC
{
LKNewConversationVC *newConversationVC = [LKNewConversationVC new];
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:newConversationVC];
@ -861,6 +865,13 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
*/
}
- (void)showNewPublicChatVC
{
LKNewPublicChatVC *newPublicChatVC = [LKNewPublicChatVC new];
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:newPublicChatVC];
[self.navigationController presentViewController:navigationController animated:YES completion:nil];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];

@ -2551,6 +2551,7 @@
"Type an optional password for added security" = "Type an optional password for added security";
"Password (Optional)" = "Password (Optional)";
"Next" = "Next";
"Add" = "Add";
"Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device." = "Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.";
"Restore your account by entering your seed below." = "Restore your account by entering your seed below.";
"Copy" = "Copy";
@ -2593,7 +2594,9 @@
"Loki Messenger" = "Loki Messenger";
"Privacy Policy" = "Privacy Policy";
"New Conversation" = "New Conversation";
"Add Public Chat Server" = "Add Public Chat Server";
"Enter a Public Key" = "Enter a Public Key";
"Enter a Server URL" = "Enter a Server URL";
"For example: 059abcf223aa8c10e3dc2d623688b75dd25896794717e4a9c486772664fc95e41e." = "For example: 059abcf223aa8c10e3dc2d623688b75dd25896794717e4a9c486772664fc95e41e.";
"Invalid Public Key" = "Invalid Public Key";
"Please check the public key you entered and try again." = "Please check the public key you entered and try again.";
@ -2635,5 +2638,12 @@
"Your device has been linked successfully" = "Your device has been linked successfully";
"Link" = "Link";
"Anonymous" = "Anonymous";
"Invalid URL" = "Invalid URL";
"Please check the URL you entered and try again." = "Please check the URL you entered and try again.";
"Please pick a shorter display name" = "Please pick a shorter display name";
"Please pick a display name" = "Please pick a display name";
"Add Public Chat" = "Add Public Chat";
"Enter a URL" = "Enter a URL";
"Enter the URL of the public chat you'd like to join. The Loki Public Chat URL is https://chat.lokinet.org." = "Enter the URL of the public chat you'd like to join. The Loki Public Chat URL is https://chat.lokinet.org.";
"Connecting..." = "Connecting...";
"Couldn't Connect" = "Couldn't Connect";

@ -40,14 +40,18 @@ NS_ASSUME_NONNULL_BEGIN
// Used for persisted quoted replies, both incoming and outgoing.
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
threadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction;
// Builds a not-yet-sent QuotedReplyModel
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(id<ConversationViewItem>)conversationItem
threadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction;
- (TSQuotedMessage *)buildQuotedMessageForSending;
// Loki
@property (nonatomic, readonly) NSString *threadId;
@end

@ -31,7 +31,8 @@ NS_ASSUME_NONNULL_BEGIN
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed NS_DESIGNATED_INITIALIZER;
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed
threadId:(NSString *)threadId NS_DESIGNATED_INITIALIZER;
@end
@ -50,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed
threadId:(NSString *)threadId
{
self = [super init];
if (!self) {
@ -66,6 +68,7 @@ NS_ASSUME_NONNULL_BEGIN
_attachmentStream = attachmentStream;
_thumbnailAttachmentPointer = thumbnailAttachmentPointer;
_thumbnailDownloadFailed = thumbnailDownloadFailed;
_threadId = threadId;
return self;
}
@ -73,6 +76,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Factory Methods
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
threadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssertDebug(quotedMessage.quotedAttachments.count <= 1);
@ -112,10 +116,12 @@ NS_ASSUME_NONNULL_BEGIN
sourceFilename:attachmentInfo.sourceFilename
attachmentStream:nil
thumbnailAttachmentPointer:attachmentPointer
thumbnailDownloadFailed:thumbnailDownloadFailed];
thumbnailDownloadFailed:thumbnailDownloadFailed
threadId:threadId];
}
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(id<ConversationViewItem>)conversationItem
threadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction;
{
OWSAssertDebug(conversationItem);
@ -160,7 +166,8 @@ NS_ASSUME_NONNULL_BEGIN
sourceFilename:nil
attachmentStream:nil
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
thumbnailDownloadFailed:NO
threadId:@""];
}
NSString *_Nullable quotedText = message.body;
@ -237,7 +244,8 @@ NS_ASSUME_NONNULL_BEGIN
sourceFilename:quotedAttachment.sourceFilename
attachmentStream:quotedAttachment
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
thumbnailDownloadFailed:NO
threadId:threadId];
}
#pragma mark - Instance Methods

@ -93,6 +93,11 @@ NS_ASSUME_NONNULL_BEGIN
if ([self isVersion:previousVersion atLeast:@"2.0.0" andLessThan:@"2.3.0"] && [self.tsAccountManager isRegistered]) {
[self clearBloomFilterCache];
}
// Loki
if ([self isVersion:previousVersion lessThan:@"1.2.1"] && [self.tsAccountManager isRegistered]) {
[self updatePublicChatMapping];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:completion];
@ -162,6 +167,21 @@ NS_ASSUME_NONNULL_BEGIN
}
}
# pragma mark Loki - Upgrading to Public Chat Manager
// Versions less than or equal to 1.2.0 didn't store public chat mappings
+ (void)updatePublicChatMapping
{
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) {
TSGroupThread *thread = [TSGroupThread threadWithGroupId:chat.idAsData transaction:transaction];
if (thread != nil) {
[LKDatabaseUtilities setPublicChat:chat threadID:thread.uniqueId transaction:transaction];
}
}
}];
}
@end
NS_ASSUME_NONNULL_END

@ -535,13 +535,18 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
[[LKGroupChatAPI setDisplayName:localProfileName on:LKGroupChatAPI.publicChatServer]
.thenOn(dispatch_get_main_queue(), ^() {
successBlock();
})
.catchOn(dispatch_get_main_queue(), ^(NSError *error) {
failureBlock(error);
}) retainUntilComplete];
__block NSDictionary *publicChats;
[SSKEnvironment.shared.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChats = [LKDatabaseUtilities getAllPublicChats:transaction];
}];
NSSet *servers = [NSSet setWithArray:[publicChats.allValues map:^NSString *(LKPublicChat *publicChat) { return publicChat.server; }]];
for (NSString *server in servers) {
[[LKPublicChatAPI setDisplayName:localProfileName on:server] retainUntilComplete];
}
successBlock();
}
- (void)fetchLocalUsersProfile

@ -1,19 +0,0 @@
@objc(LKGroupChat)
public final class LokiGroupChat : NSObject {
@objc public let id: String
@objc public let serverID: UInt64
@objc public let server: String
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public init(serverID: UInt64, server: String, displayName: String, isDeletable: Bool) {
self.id = "\(server).\(serverID)"
self.serverID = serverID
self.server = server
self.displayName = displayName
self.isDeletable = isDeletable
}
override public var description: String { return displayName }
}

@ -309,10 +309,19 @@ public final class LokiAPI : NSObject {
guard let cache = userHexEncodedPublicKeyCache[threadID] else { return [] }
var candidates: [Mention] = []
// Gather candidates
var publicChat: LokiPublicChat?
storage.dbReadConnection.read { transaction in
publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction)
}
storage.dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
candidates = cache.flatMap { hexEncodedPublicKey in
guard let displayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String? else { return nil }
let uncheckedDisplayName: String?
if let publicChat = publicChat {
uncheckedDisplayName = DisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server)
} else {
uncheckedDisplayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey)
}
guard let displayName = uncheckedDisplayName else { return nil }
guard !displayName.hasPrefix("Anonymous") else { return nil }
return Mention(hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName)
}

@ -1,7 +1,7 @@
import PromiseKit
@objc(LKLongPoller)
public final class LongPoller : NSObject {
public final class LokiLongPoller : NSObject {
private let onMessagesReceived: ([SSKProtoEnvelope]) -> Void
private let storage = OWSPrimaryStorage.shared()
private var hasStarted = false

@ -0,0 +1,3 @@
public struct LokiPublicChannel {
public let name: String
}

@ -0,0 +1,43 @@
@objc(LKPublicChat)
public final class LokiPublicChat : NSObject, NSCoding {
@objc public let id: String
@objc public let idAsData: Data
@objc public let channel: UInt64
@objc public let server: String
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public init?(channel: UInt64, server: String, displayName: String, isDeletable: Bool) {
let id = "\(server).\(channel)"
self.id = id
guard let idAsData = id.data(using: .utf8) else { return nil }
self.idAsData = idAsData
self.channel = channel
self.server = server.lowercased()
self.displayName = displayName
self.isDeletable = isDeletable
}
// MARK: Coding
@objc public init?(coder: NSCoder) {
channel = UInt64(coder.decodeInt64(forKey: "channel"))
server = coder.decodeObject(forKey: "server") as! String
let id = "\(server).\(channel)"
self.id = id
guard let idAsData = id.data(using: .utf8) else { return nil }
self.idAsData = idAsData
displayName = coder.decodeObject(forKey: "displayName") as! String
isDeletable = coder.decodeBool(forKey: "isDeletable")
super.init()
}
@objc public func encode(with coder: NSCoder) {
coder.encode(Int64(channel), forKey: "channel")
coder.encode(server, forKey: "server")
coder.encode(displayName, forKey: "displayName")
coder.encode(isDeletable, forKey: "isDeletable")
}
override public var description: String { return "\(displayName) (\(server))" }
}

@ -1,7 +1,7 @@
import PromiseKit
@objc(LKGroupChatAPI)
public final class LokiGroupChatAPI : LokiDotNetAPI {
@objc(LKPublicChatAPI)
public final class LokiPublicChatAPI : LokiDotNetAPI {
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
// MARK: Settings
@ -9,13 +9,17 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
private static let maxRetryCount: UInt = 8
// MARK: Public Chat
#if DEBUG
@objc public static let publicChatServer = "https://chat-dev.lokinet.org"
#else
@objc public static let publicChatServer = "https://chat.lokinet.org"
#endif
@objc private static let channelInfoType = "net.patter-app.settings"
@objc public static let publicChatMessageType = "network.loki.messenger.publicChat"
@objc public static let publicChatServerID: UInt64 = 1
@objc public static let defaultChats: [LokiPublicChat] = {
var result: [LokiPublicChat] = []
result.append(LokiPublicChat(channel: 1, server: "https://chat.lokinet.org", displayName: NSLocalizedString("Loki Public Chat", comment: ""), isDeletable: true)!)
#if DEBUG
result.append(LokiPublicChat(channel: 1, server: "https://chat-dev.lokinet.org", displayName: "Loki Dev Chat", isDeletable: true)!)
#endif
return result
}()
// MARK: Convenience
private static var userDisplayName: String {
@ -23,9 +27,9 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
// MARK: Database
override internal class var authTokenCollection: String { "LokiGroupChatAuthTokenCollection" }
private static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
private static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
override internal class var authTokenCollection: String { "LokiGroupChatAuthTokenCollection" } // Should ideally be LokiPublicChatAuthTokenCollection
private static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection" // Should ideally be LokiPublicChatLastMessageServerIDCollection
private static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection" // Should ideally be LokiPublicChatLastDeletionServerIDCollection
private static func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? {
var result: UInt? = nil
@ -41,6 +45,12 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
}
private static func removeLastMessageServerID(for group: UInt64, on server: String) {
storage.dbReadWriteConnection.readWrite { transaction in
transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection)
}
}
private static func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt? {
var result: UInt? = nil
storage.dbReadConnection.read { transaction in
@ -55,20 +65,26 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
}
private static func removeLastDeletionServerID(for group: UInt64, on server: String) {
storage.dbReadWriteConnection.readWrite { transaction in
transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection)
}
}
// MARK: Public API
public static func getMessages(for group: UInt64, on server: String) -> Promise<[LokiGroupMessage]> {
print("[Loki] Getting messages for group chat with ID: \(group) on server: \(server).")
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[LokiPublicChatMessage]> {
print("[Loki] Getting messages for public chat channel with ID: \(channel) on server: \(server).")
var queryParameters = "include_annotations=1"
if let lastMessageServerID = getLastMessageServerID(for: group, on: server) {
if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) {
queryParameters += "&since_id=\(lastMessageServerID)"
} else {
queryParameters += "&count=-\(fallbackBatchCount)"
}
let url = URL(string: "\(server)/channels/\(group)/messages?\(queryParameters)")!
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
return rawMessages.flatMap { message in
@ -78,23 +94,23 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64,
let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String,
let timestamp = value["timestamp"] as? UInt64 else {
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(message).")
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).")
return nil
}
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
let lastMessageServerID = getLastMessageServerID(for: group, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: group, on: server, to: serverID) }
let quote: LokiGroupMessage.Quote?
let lastMessageServerID = getLastMessageServerID(for: channel, on: server)
if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: channel, on: server, to: serverID) }
let quote: LokiPublicChatMessage.Quote?
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteeHexEncodedPublicKey = quoteAsJSON["author"] as? String, let quotedMessageBody = quoteAsJSON["text"] as? String {
let quotedMessageServerID = message["reply_to"] as? UInt64
quote = LokiGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
quote = LokiPublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteeHexEncodedPublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
} else {
quote = nil
}
let signature = LokiGroupMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let result = LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, signature: signature)
let signature = LokiPublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
let result = LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, signature: signature)
guard result.hasValidSignature() else {
print("[Loki] Ignoring group chat message with invalid signature.")
print("[Loki] Ignoring public chat message with invalid signature.")
return nil
}
return result
@ -102,11 +118,11 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
}
public static func sendMessage(_ message: LokiGroupMessage, to group: UInt64, on server: String) -> Promise<LokiGroupMessage> {
public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise<LokiPublicChatMessage> {
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: Error.signingFailed) }
return getAuthToken(for: server).then { token -> Promise<LokiGroupMessage> in
print("[Loki] Sending message to group chat with ID: \(group) on server: \(server).")
let url = URL(string: "\(server)/channels/\(group)/messages")!
return getAuthToken(for: server).then { token -> Promise<LokiPublicChatMessage> in
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
let url = URL(string: "\(server)/channels/\(channel)/messages")!
let parameters = signedMessage.toJSON()
let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
@ -117,13 +133,13 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, signature: signedMessage.signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, signature: signedMessage.signature)
}
}.recover { error -> Promise<LokiGroupMessage> in
}.recover { error -> Promise<LokiPublicChatMessage> in
if let error = error as? NetworkManagerError, error.statusCode == 401 {
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
@ -132,44 +148,44 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}.retryingIfNeeded(maxRetryCount: maxRetryCount).map { message in
Analytics.shared.track("Group Message Sent")
return message
}.recover { error -> Promise<LokiGroupMessage> in
}.recover { error -> Promise<LokiPublicChatMessage> in
Analytics.shared.track("Failed to Send Group Message")
throw error
}
}
public static func getDeletedMessageServerIDs(for group: UInt64, on server: String) -> Promise<[UInt64]> {
print("[Loki] Getting deleted messages for group chat with ID: \(group) on server: \(server).")
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
print("[Loki] Getting deleted messages for public chat channel with ID: \(channel) on server: \(server).")
let queryParameters: String
if let lastDeletionServerID = getLastDeletionServerID(for: group, on: server) {
if let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) {
queryParameters = "since_id=\(lastDeletionServerID)"
} else {
queryParameters = "count=\(fallbackBatchCount)"
}
let url = URL(string: "\(server)/loki/v1/channel/\(group)/deletes?\(queryParameters)")!
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
print("[Loki] Couldn't parse deleted messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
return deletions.flatMap { deletion in
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
print("[Loki] Couldn't parse deleted message for group chat with ID: \(group) on server: \(server) from: \(deletion).")
print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).")
return nil
}
let lastDeletionServerID = getLastDeletionServerID(for: group, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: group, on: server, to: serverID) }
let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server)
if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: channel, on: server, to: serverID) }
return messageServerID
}
}
}
public static func deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
return getAuthToken(for: server).then { token -> Promise<Void> in
let isModerationRequest = !isSentByUser
print("[Loki] Deleting message with ID: \(messageID) for group chat with ID: \(group) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = isSentByUser ? "\(server)/channels/\(group)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
let url = URL(string: urlAsString)!
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
@ -179,27 +195,27 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
}
public static func getModerators(for group: UInt64, on server: String) -> Promise<Set<String>> {
let url = URL(string: "\(server)/loki/v1/channel/\(group)/get_moderators")!
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
print("[Loki] Couldn't parse moderators for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
let moderatorAsSet = Set(moderators);
if self.moderators.keys.contains(server) {
self.moderators[server]![group] = moderatorAsSet
self.moderators[server]![channel] = moderatorAsSet
} else {
self.moderators[server] = [ group : moderatorAsSet ]
self.moderators[server] = [ channel : moderatorAsSet ]
}
return moderatorAsSet
}
}
@objc (isUserModerator:forGroup:onServer:)
public static func isUserModerator(_ hexEncodedPublicString: String, for group: UInt64, on server: String) -> Bool {
return moderators[server]?[group]?.contains(hexEncodedPublicString) ?? false
public static func isUserModerator(_ hexEncodedPublicString: String, for channel: UInt64, on server: String) -> Bool {
return moderators[server]?[channel]?.contains(hexEncodedPublicString) ?? false
}
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
@ -213,9 +229,31 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
print("Couldn't update display name due to error: \(error).")
throw error
}
}.retryingIfNeeded(maxRetryCount: 3)
}
public static func getInfo(for channel: UInt64, on server: String) -> Promise<LokiPublicChatInfo> {
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
let request = TSRequest(url: url)
return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in
guard let json = rawResponse as? JSON,
let data = json["data"] as? JSON,
let annotations = data["annotations"] as? [JSON],
let annotation = annotations.first,
let info = annotation["value"] as? JSON,
let displayName = info["name"] as? String else {
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
throw Error.parsingFailed
}
return LokiPublicChatInfo(displayName: displayName)
}
}
public static func clearCaches(for channel: UInt64, on server: String) {
removeLastMessageServerID(for: channel, on: server)
removeLastDeletionServerID(for: channel, on: server)
}
// MARK: Public API (Obj-C)
@objc(getMessagesForGroup:onServer:)
public static func objc_getMessages(for group: UInt64, on server: String) -> AnyPromise {
@ -223,7 +261,7 @@ public final class LokiGroupChatAPI : LokiDotNetAPI {
}
@objc(sendMessage:toGroup:onServer:)
public static func objc_sendMessage(_ message: LokiGroupMessage, to group: UInt64, on server: String) -> AnyPromise {
public static func objc_sendMessage(_ message: LokiPublicChatMessage, to group: UInt64, on server: String) -> AnyPromise {
return AnyPromise.from(sendMessage(message, to: group, on: server))
}

@ -0,0 +1,4 @@
public struct LokiPublicChatInfo {
public let displayName: String
}

@ -0,0 +1,142 @@
import PromiseKit
@objc(LKPublicChatManager)
public final class LokiPublicChatManager : NSObject {
private let storage = OWSPrimaryStorage.shared()
private var chats: [String:LokiPublicChat] = [:]
private var pollers: [String:LokiPublicChatPoller] = [:]
private var isPolling = false
private var userHexEncodedPublicKey: String? {
return OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey
}
public enum Error : Swift.Error {
case chatCreationFailed
case userPublicKeyNotFound
}
@objc public static let shared = LokiPublicChatManager()
private override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(onThreadDeleted(_:)), name: .threadDeleted, object: nil)
refreshChatsAndPollers()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc public func startPollersIfNeeded() {
for (threadID, publicChat) in chats {
if let poller = pollers[threadID] {
poller.startIfNeeded()
} else {
let poller = LokiPublicChatPoller(for: publicChat)
poller.startIfNeeded()
pollers[threadID] = poller
}
}
isPolling = true
}
@objc public func stopPollers() {
for poller in pollers.values { poller.stop() }
isPolling = false
}
public func addChat(server: String, channel: UInt64) -> Promise<LokiPublicChat> {
if let existingChat = getChat(server: server, channel: channel) {
if let newChat = self.addChat(server: server, channel: channel, name: existingChat.displayName) {
return Promise.value(newChat)
} else {
return Promise(error: Error.chatCreationFailed)
}
}
return LokiPublicChatAPI.getAuthToken(for: server).then { token in
return LokiPublicChatAPI.getInfo(for: channel, on: server)
}.map { channelInfo -> LokiPublicChat in
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
return chat
}
}
@discardableResult
@objc(addChatWithServer:channel:name:)
public func addChat(server: String, channel: UInt64, name: String) -> LokiPublicChat? {
guard let chat = LokiPublicChat(channel: channel, server: server, displayName: name, isDeletable: true) else { return nil }
let model = TSGroupModel(title: chat.displayName, memberIds: [userHexEncodedPublicKey!, chat.server], image: nil, groupId: chat.idAsData)
// Store the group chat mapping
self.storage.dbReadWriteConnection.readWrite { transaction in
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
// Mute the thread
if let utc = TimeZone(identifier: "UTC") {
var calendar = Calendar.current
calendar.timeZone = utc
var dateComponents = DateComponents()
dateComponents.setValue(999, for: .year)
if let date = calendar.date(byAdding: dateComponents, to: Date()) {
thread.updateWithMuted(until: date, transaction: transaction)
}
}
// Save the group chat
LokiDatabaseUtilities.setPublicChat(chat, for: thread.uniqueId!, in: transaction)
}
// Update chats and pollers
self.refreshChatsAndPollers()
return chat
}
@objc(addChatWithServer:channel:)
public func objc_addChat(server: String, channel: UInt64) -> AnyPromise {
return AnyPromise.from(addChat(server: server, channel: channel))
}
private func refreshChatsAndPollers() {
storage.dbReadConnection.read { transaction in
let newChats = LokiDatabaseUtilities.getAllPublicChats(in: transaction)
// Remove any chats that don't exist in the database
let removedChatThreadIds = self.chats.keys.filter { !newChats.keys.contains($0) }
removedChatThreadIds.forEach { threadID in
let poller = self.pollers.removeValue(forKey: threadID)
poller?.stop()
}
// Only append to chats if we have a thread for the chat
self.chats = newChats.filter { (threadID, group) in
return TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) != nil
}
}
if (isPolling) { startPollersIfNeeded() }
}
@objc private func onThreadDeleted(_ notification: Notification) {
guard let threadId = notification.userInfo?["threadId"] as? String else { return }
// Reset the last message cache
if let chat = self.chats[threadId] {
LokiPublicChatAPI.clearCaches(for: chat.channel, on: chat.server)
}
// Remove the chat from the db
storage.dbReadWriteConnection.readWrite { transaction in
LokiDatabaseUtilities.removePublicChat(for: threadId, in: transaction)
}
refreshChatsAndPollers()
}
private func getChat(server: String, channel: UInt64) -> LokiPublicChat? {
return chats.values.first { chat in
return chat.server == server && chat.channel == channel
}
}
}

@ -1,7 +1,7 @@
import PromiseKit
@objc(LKGroupMessage)
public final class LokiGroupMessage : NSObject {
public final class LokiPublicChatMessage : NSObject {
public let serverID: UInt64?
public let hexEncodedPublicKey: String
public let displayName: String
@ -62,18 +62,18 @@ public final class LokiGroupMessage : NSObject {
}
// MARK: Crypto
internal func sign(with privateKey: Data) -> LokiGroupMessage? {
internal func sign(with privateKey: Data) -> LokiPublicChatMessage? {
guard let data = getValidationData(for: signatureVersion) else {
print("[Loki] Failed to sign group chat message.")
print("[Loki] Failed to sign public chat message.")
return nil
}
let userKeyPair = OWSIdentityManager.shared().identityKeyPair()!
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
print("[Loki] Failed to sign group chat message.")
print("[Loki] Failed to sign public chat message.")
return nil
}
let signature = Signature(data: signatureData, version: signatureVersion)
return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, signature: signature)
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: type, timestamp: timestamp, quote: quote, signature: signature)
}
internal func hasValidSignature() -> Bool {

@ -1,7 +1,7 @@
@objc(LKGroupChatPoller)
public final class GroupChatPoller : NSObject {
private let group: LokiGroupChat
@objc(LKPublicChatPoller)
public final class LokiPublicChatPoller : NSObject {
private let publicChat: LokiPublicChat
private var pollForNewMessagesTimer: Timer? = nil
private var pollForDeletedMessagesTimer: Timer? = nil
private var pollForModeratorsTimer: Timer? = nil
@ -14,9 +14,9 @@ public final class GroupChatPoller : NSObject {
private let pollForModeratorsInterval: TimeInterval = 10 * 60
// MARK: Lifecycle
@objc(initForGroup:)
public init(for group: LokiGroupChat) {
self.group = group
@objc(initForPublicChat:)
public init(for publicChat: LokiPublicChat) {
self.publicChat = publicChat
super.init()
}
@ -42,17 +42,17 @@ public final class GroupChatPoller : NSObject {
// MARK: Polling
private func pollForNewMessages() {
// Prepare
let group = self.group
let publicChat = self.publicChat
let userHexEncodedPublicKey = self.userHexEncodedPublicKey
// Processing logic for incoming messages
func processIncomingMessage(_ message: LokiGroupMessage) {
func processIncomingMessage(_ message: LokiPublicChatMessage) {
let senderHexEncodedPublicKey = message.hexEncodedPublicKey
let endIndex = senderHexEncodedPublicKey.endIndex
let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8)
let senderDisplayName = "\(message.displayName) (...\(senderHexEncodedPublicKey[cutoffIndex..<endIndex]))"
let id = group.id.data(using: String.Encoding.utf8)!
let id = publicChat.idAsData
let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver)
groupContext.setName(group.displayName)
groupContext.setName(publicChat.displayName)
let dataMessage = SSKProtoDataMessage.builder()
dataMessage.setTimestamp(message.timestamp)
dataMessage.setGroup(try! groupContext.build())
@ -75,12 +75,12 @@ public final class GroupChatPoller : NSObject {
envelope.setContent(try! content.build().serializedData())
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
transaction.setObject(senderDisplayName, forKey: senderHexEncodedPublicKey, inCollection: group.id)
transaction.setObject(senderDisplayName, forKey: senderHexEncodedPublicKey, inCollection: publicChat.id)
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction)
}
}
// Processing logic for outgoing messages
func processOutgoingMessage(_ message: LokiGroupMessage) {
func processOutgoingMessage(_ message: LokiPublicChatMessage) {
guard let messageServerID = message.serverID else { return }
let storage = OWSPrimaryStorage.shared()
var isDuplicate = false
@ -89,7 +89,7 @@ public final class GroupChatPoller : NSObject {
isDuplicate = id != nil
}
guard !isDuplicate else { return }
guard let groupID = group.id.data(using: .utf8) else { return }
let groupID = publicChat.idAsData
let thread = TSGroupThread.getOrCreateThread(withGroupId: groupID)
let signalQuote: TSQuotedMessage?
if let quote = message.quote {
@ -100,9 +100,9 @@ public final class GroupChatPoller : NSObject {
let message = TSOutgoingMessage(outgoingMessageWithTimestamp: message.timestamp, in: thread, messageBody: message.body, attachmentIds: [], expiresInSeconds: 0,
expireStartedAt: 0, isVoiceMessage: false, groupMetaMessage: .deliver, quotedMessage: signalQuote, contactShare: nil, linkPreview: nil)
storage.dbReadWriteConnection.readWrite { transaction in
message.update(withSentRecipient: group.server, wasSentByUD: false, transaction: transaction)
message.update(withSentRecipient: publicChat.server, wasSentByUD: false, transaction: transaction)
message.saveGroupChatServerID(messageServerID, in: transaction)
guard let messageID = message.uniqueId else { return print("[Loki] Failed to save group message.") }
guard let messageID = message.uniqueId else { return print("[Loki] Failed to save public chat message.") }
storage.setIDForMessageWithServerID(UInt(messageServerID), to: messageID, in: transaction)
}
if let linkPreviewURL = OWSLinkPreview.previewUrl(forMessageBodyText: message.body, selectedRange: nil) {
@ -110,7 +110,7 @@ public final class GroupChatPoller : NSObject {
}
}
// Poll
let _ = LokiGroupChatAPI.getMessages(for: group.serverID, on: group.server).done(on: .main) { messages in
let _ = LokiPublicChatAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: .main) { messages in
messages.forEach { message in
if message.hexEncodedPublicKey != userHexEncodedPublicKey {
processIncomingMessage(message)
@ -122,8 +122,8 @@ public final class GroupChatPoller : NSObject {
}
private func pollForDeletedMessages() {
let group = self.group
let _ = LokiGroupChatAPI.getDeletedMessageServerIDs(for: group.serverID, on: group.server).done { deletedMessageServerIDs in
let publicChat = self.publicChat
let _ = LokiPublicChatAPI.getDeletedMessageServerIDs(for: publicChat.channel, on: publicChat.server).done { deletedMessageServerIDs in
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
let deletedMessageIDs = deletedMessageServerIDs.compactMap { storage.getIDForMessage(withServerID: UInt($0), in: transaction) }
@ -135,6 +135,6 @@ public final class GroupChatPoller : NSObject {
}
private func pollForModerators() {
let _ = LokiGroupChatAPI.getModerators(for: group.serverID, on: group.server)
let _ = LokiPublicChatAPI.getModerators(for: publicChat.channel, on: publicChat.server)
}
}

@ -4,6 +4,7 @@ public final class LokiDatabaseUtilities : NSObject {
private override init() { }
// MARK: Quotes
@objc(getServerIDForQuoteWithID:quoteeHexEncodedPublicKey:threadID:transaction:)
public static func getServerID(quoteID: UInt64, quoteeHexEncodedPublicKey: String, threadID: String, transaction: YapDatabaseReadTransaction) -> UInt64 {
guard let message = TSInteraction.interactions(withTimestamp: quoteID, filter: { interaction in
@ -20,8 +21,37 @@ public final class LokiDatabaseUtilities : NSObject {
return message.groupChatServerID
}
// MARK: Device Links
@objc(getMasterHexEncodedPublicKeyFor:in:)
public static func objc_getMasterHexEncodedPublicKey(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> String? {
return OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: slaveHexEncodedPublicKey, in: transaction)
}
// MARK: Public Chats
private static let publicChatCollection = "LokiPublicChatCollection"
@objc(getAllPublicChats:)
public static func getAllPublicChats(in transaction: YapDatabaseReadTransaction) -> [String:LokiPublicChat] {
var result = [String:LokiPublicChat]()
transaction.enumerateKeysAndObjects(inCollection: publicChatCollection) { threadID, object, _ in
guard let publicChat = object as? LokiPublicChat else { return }
result[threadID] = publicChat
}
return result
}
@objc(getPublicChatForThreadID:transaction:)
public static func getPublicChat(for threadID: String, in transaction: YapDatabaseReadTransaction) -> LokiPublicChat? {
return transaction.object(forKey: threadID, inCollection: publicChatCollection) as? LokiPublicChat
}
@objc(setPublicChat:threadID:transaction:)
public static func setPublicChat(_ publicChat: LokiPublicChat, for threadID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(publicChat, forKey: threadID, inCollection: publicChatCollection)
}
@objc(removePublicChatForThreadID:transaction:)
public static func removePublicChat(for threadID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: threadID, inCollection: publicChatCollection)
}
}

@ -1,7 +1,7 @@
public extension OWSPrimaryStorage {
private func getCollection(for primaryDevice: String) -> String {
private func getDeviceLinkCollection(for primaryDevice: String) -> String {
return "LokiDeviceLinkCollection-\(primaryDevice)"
}
@ -13,23 +13,23 @@ public extension OWSPrimaryStorage {
return
}
let masterHexEncodedPublicKey = masterHexEncodedPublicKeys.first!
let collection = getCollection(for: masterHexEncodedPublicKey)
let collection = getDeviceLinkCollection(for: masterHexEncodedPublicKey)
transaction.removeAllObjects(inCollection: collection)
deviceLinks.forEach { addDeviceLink($0, in: transaction) } // TODO: Check the performance impact of this
}
public func addDeviceLink(_ deviceLink: DeviceLink, in transaction: YapDatabaseReadWriteTransaction) {
let collection = getCollection(for: deviceLink.master.hexEncodedPublicKey)
let collection = getDeviceLinkCollection(for: deviceLink.master.hexEncodedPublicKey)
transaction.setObject(deviceLink, forKey: deviceLink.slave.hexEncodedPublicKey, inCollection: collection)
}
public func removeDeviceLink(_ deviceLink: DeviceLink, in transaction: YapDatabaseReadWriteTransaction) {
let collection = getCollection(for: deviceLink.master.hexEncodedPublicKey)
let collection = getDeviceLinkCollection(for: deviceLink.master.hexEncodedPublicKey)
transaction.removeObject(forKey: deviceLink.slave.hexEncodedPublicKey, inCollection: collection)
}
public func getDeviceLinks(for masterHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set<DeviceLink> {
let collection = getCollection(for: masterHexEncodedPublicKey)
let collection = getDeviceLinkCollection(for: masterHexEncodedPublicKey)
var result: Set<DeviceLink> = []
transaction.enumerateRows(inCollection: collection) { _, object, _, _ in
guard let deviceLink = object as? DeviceLink else { return }

@ -0,0 +1,39 @@
@objc(LKDisplayNameUtilities)
public final class DisplayNameUtilities : NSObject {
override private init() { }
private static var userHexEncodedPublicKey: String {
return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
}
private static var userDisplayName: String? {
return SSKEnvironment.shared.profileManager.localProfileName()!
}
@objc public static func getPrivateChatDisplayName(for hexEncodedPublicKey: String) -> String? {
if hexEncodedPublicKey == userHexEncodedPublicKey {
return userDisplayName
} else {
return SSKEnvironment.shared.profileManager.profileName(forRecipientId: hexEncodedPublicKey)
}
}
@objc public static func getPublicChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String) -> String? {
var result: String?
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
result = getPublicChatDisplayName(for: hexEncodedPublicKey, in: channel, on: server, using: transaction)
}
return result
}
@objc public static func getPublicChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String, using transaction: YapDatabaseReadTransaction) -> String? {
if hexEncodedPublicKey == userHexEncodedPublicKey {
return userDisplayName
} else {
let collection = "\(server).\(channel)"
return transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String?
}
}
}

@ -46,8 +46,8 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
@property (nonatomic, readonly) BOOL hasFriendRequestStatusMessage;
@property (nonatomic) BOOL isP2P;
// Group chat
@property (nonatomic) uint64_t groupChatServerID;
@property (nonatomic, readonly) BOOL isGroupChatMessage;
@property (nonatomic) uint64_t groupChatServerID; // Should ideally be publicChatServerID
@property (nonatomic, readonly) BOOL isGroupChatMessage; // Should ideally be isPublicChatMessage
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE;

@ -503,7 +503,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if ([message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
[recipientIds addObject:self.tsAccountManager.localNumber];
} else if (thread.isGroupThread) {
[recipientIds addObject:LKGroupChatAPI.publicChatServer];
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
LKPublicChat *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
if (publicChat != nil) {
[recipientIds addObject:publicChat.server];
} else {
// TODO: Handle
}
}];
} else if ([thread isKindOfClass:[TSContactThread class]]) {
NSString *recipientContactId = ((TSContactThread *)thread).contactIdentifier;
@ -1185,7 +1192,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData];
};
if ([recipient.recipientId isEqualToString:LKGroupChatAPI.publicChatServer]) {
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:message.uniqueThreadId transaction: transaction];
}];
if (publicChat != nil) {
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName;
if (displayName == nil) { displayName = @"Anonymous"; }
@ -1198,9 +1209,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey threadID:messageSend.thread.uniqueId transaction:transaction];
}];
}
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:message.body type:LKGroupChatAPI.publicChatMessageType
LKGroupMessage *groupMessage = [[LKGroupMessage alloc] initWithHexEncodedPublicKey:userHexEncodedPublicKey displayName:displayName body:message.body type:LKPublicChatAPI.publicChatMessageType
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteeHexEncodedPublicKey:quoteeHexEncodedPublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0];
[[LKGroupChatAPI sendMessage:groupMessage toGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer]
[[LKPublicChatAPI sendMessage:groupMessage toGroup:publicChat.channel onServer:publicChat.server]
.thenOn(OWSDispatch.sendingQueue, ^(LKGroupMessage *groupMessage) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message saveGroupChatServerID:groupMessage.serverID in:transaction];

@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
- (OWSAES256Key *)localProfileKey;
- (nullable NSString *)localProfileName;
- (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId;
- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;

Loading…
Cancel
Save