diff --git a/Pods b/Pods
index 67dbced37..2870e676d 160000
--- a/Pods
+++ b/Pods
@@ -1 +1 @@
-Subproject commit 67dbced37481e0011a3df1397ed57711384a4957
+Subproject commit 2870e676deec6a7ddb931edb6f0284f1f5b36085
diff --git a/Signal/src/AppDelegate.h b/Signal/src/AppDelegate.h
index 3cf39a281..e36cdadca 100644
--- a/Signal/src/AppDelegate.h
+++ b/Signal/src/AppDelegate.h
@@ -11,6 +11,8 @@ extern NSString *const AppDelegateStoryboardMain;
 - (void)startLongPollerIfNeeded;
 - (void)stopLongPollerIfNeeded;
 - (void)setUpDefaultPublicChatsIfNeeded;
+- (void)startOpenGroupPollersIfNeeded;
+- (void)stopOpenGroupPollersIfNeeded;
 - (void)createRSSFeedsIfNeeded;
 - (void)startRSSFeedPollersIfNeeded;
 
diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m
index 3039232bf..25a8de660 100644
--- a/Signal/src/AppDelegate.m
+++ b/Signal/src/AppDelegate.m
@@ -177,7 +177,9 @@ static NSTimeInterval launchStartedAt;
 
     [DDLog flushLog];
 
+    // Loki: Stop pollers
     [self stopLongPollerIfNeeded];
+    [self stopOpenGroupPollersIfNeeded];
 }
 
 - (void)applicationWillEnterForeground:(UIApplication *)application
@@ -195,8 +197,10 @@ static NSTimeInterval launchStartedAt;
     OWSLogInfo(@"applicationWillTerminate.");
 
     [DDLog flushLog];
-    
+
+    // Loki: Stop pollers
     [self stopLongPollerIfNeeded];
+    [self stopOpenGroupPollersIfNeeded];
 
     if (self.lokiP2PServer) { [self.lokiP2PServer stop]; }
 }
@@ -316,9 +320,6 @@ static NSTimeInterval launchStartedAt;
                                                  name:NSNotificationName_2FAStateDidChange
                                                object:nil];
     
-    // Loki - Observe new messages received notifications
-    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleNewMessagesReceived:) name:NSNotification.newMessagesReceived object:nil];
-    
     // Loki - Observe thread deleted notifications
     [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleThreadDeleted:) name:NSNotification.threadDeleted object:nil];
 
@@ -770,14 +771,32 @@ static NSTimeInterval launchStartedAt;
             [self.socketManager requestSocketOpen];
             [Environment.shared.contactsManager fetchSystemContactsOnceIfAlreadyAuthorized];
 
+            NSString *userHexEncodedPublicKey = self.tsAccountManager.localNumber;
+
             // Loki: Tell our friends that we are online
             [LKP2PAPI broadcastOnlineStatus];
 
-            // Loki: Start long polling
+            // Loki: Start pollers
             [self startLongPollerIfNeeded];
+            [self startOpenGroupPollersIfNeeded];
 
             // Loki: Get device links
-            [LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber];
+            [[LKFileServerAPI getDeviceLinksAssociatedWith:userHexEncodedPublicKey] retainUntilComplete];
+
+            // Loki: Update profile picture if needed
+            NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
+            NSDate *now = [NSDate new];
+            NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"];
+            if (lastProfilePictureUpload != nil && [now timeIntervalSinceDate:lastProfilePictureUpload] > 14 * 24 * 60 * 60) {
+                OWSProfileManager *profileManager = OWSProfileManager.sharedManager;
+                NSString *displayName = [profileManager profileNameForRecipientId:userHexEncodedPublicKey];
+                UIImage *profilePicture = [profileManager profileAvatarForRecipientId:userHexEncodedPublicKey];
+                [profileManager updateLocalProfileName:displayName avatarImage:profilePicture success:^{
+                    // Do nothing; the user defaults flag is updated in LokiFileServerAPI
+                } failure:^(NSError *error) {
+                    // Do nothing
+                } requiresSync:YES];
+            }
             
             if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) {
                 OWSLogInfo(@"Retrying to register for remote notifications since user hasn't registered yet.");
@@ -1118,6 +1137,8 @@ static NSTimeInterval launchStartedAt;
         OWSLogInfo(@"Ignoring remote notification; app not ready.");
         return;
     }
+    
+    CurrentAppContext().wasWokenUpBySilentPushNotification = true;
 
     [LKLogger print:@"[Loki] Silent push notification received; fetching messages."];
     
@@ -1135,7 +1156,7 @@ static NSTimeInterval launchStartedAt;
     [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
         publicChats = [LKDatabaseUtilities getAllPublicChats:transaction];
     }];
-    for (LKPublicChat *publicChat in publicChats) {
+    for (LKPublicChat *publicChat in publicChats.allValues) {
         if (![publicChat isKindOfClass:LKPublicChat.class]) { continue; } // For some reason publicChat is sometimes a base 64 encoded string...
         LKPublicChatPoller *poller = [[LKPublicChatPoller alloc] initForPublicChat:publicChat];
         [poller stop];
@@ -1146,8 +1167,12 @@ static NSTimeInterval launchStartedAt;
     
     PMKJoin(promises).then(^(id results) {
         completionHandler(UIBackgroundFetchResultNewData);
+        CurrentAppContext().wasWokenUpBySilentPushNotification = false;
+        [LKLogger print:@"[Loki] UIBackgroundFetchResultNewData"];
     }).catch(^(id error) {
         completionHandler(UIBackgroundFetchResultFailed);
+        CurrentAppContext().wasWokenUpBySilentPushNotification = false;
+        [LKLogger print:@"[Loki] UIBackgroundFetchResultFailed"];
     });
 }
 
@@ -1444,11 +1469,12 @@ static NSTimeInterval launchStartedAt;
         // Loki: Start friend request expiration job
         [self.lokiFriendRequestExpirationJob startIfNecessary];
         
-        // Loki: Start long polling
+        // Loki: Start pollers
         [self startLongPollerIfNeeded];
+        [self startOpenGroupPollersIfNeeded];
 
         // Loki: Get device links
-        [LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber];
+        [[LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber] retainUntilComplete]; // TODO: Is this even needed?
     }
 }
 
@@ -1586,16 +1612,6 @@ static NSTimeInterval launchStartedAt;
     [self.lokiLongPoller stopIfNeeded];
 }
 
-- (LKRSSFeed *)lokiNewsFeed
-{
-    return [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:@"Loki News" isDeletable:true];
-}
-
-- (LKRSSFeed *)lokiMessengerUpdatesFeed
-{
-    return [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:@"Session Updates" isDeletable:false];
-}
-
 - (void)setUpDefaultPublicChatsIfNeeded
 {
     for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) {
@@ -1612,6 +1628,27 @@ static NSTimeInterval launchStartedAt;
     }
 }
 
+- (void)startOpenGroupPollersIfNeeded
+{
+    [LKPublicChatManager.shared startPollersIfNeeded];
+    [SSKEnvironment.shared.attachmentDownloads continueDownloadIfPossible];
+}
+
+- (void)stopOpenGroupPollersIfNeeded
+{
+    [LKPublicChatManager.shared stopPollers];
+}
+
+- (LKRSSFeed *)lokiNewsFeed
+{
+    return [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:@"Loki News" isDeletable:true];
+}
+
+- (LKRSSFeed *)lokiMessengerUpdatesFeed
+{
+    return [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:@"Session Updates" isDeletable:false];
+}
+
 - (void)createRSSFeedsIfNeeded
 {
     NSArray *feeds = @[ /*self.lokiNewsFeed,*/ self.lokiMessengerUpdatesFeed ];
@@ -1670,6 +1707,7 @@ static NSTimeInterval launchStartedAt;
     [SSKEnvironment.shared.identityManager clearIdentityKey];
     [LKAPI clearRandomSnodePool];
     [self stopLongPollerIfNeeded];
+    [self stopOpenGroupPollersIfNeeded];
     [self.lokiNewsFeedPoller stop];
     [self.lokiMessengerUpdatesFeedPoller stop];
     [LKPublicChatManager.shared stopPollers];
diff --git a/Signal/src/Loki/Components/ConversationTitleView.swift b/Signal/src/Loki/Components/ConversationTitleView.swift
index 411b19b79..6f9103012 100644
--- a/Signal/src/Loki/Components/ConversationTitleView.swift
+++ b/Signal/src/Loki/Components/ConversationTitleView.swift
@@ -7,8 +7,8 @@ final class ConversationTitleView : UIView {
     // MARK: Types
     private enum Status : Int {
         case calculatingPoW = 1
-        case contactingNetwork = 2
-        case sendingMessage = 3
+        case routing = 2
+        case messageSending = 3
         case messageSent = 4
         case messageFailed = 5
     }
@@ -40,8 +40,8 @@ final class ConversationTitleView : UIView {
         let notificationCenter = NotificationCenter.default
         notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
         notificationCenter.addObserver(self, selector: #selector(handleCalculatingPoWNotification(_:)), name: .calculatingPoW, object: nil)
-        notificationCenter.addObserver(self, selector: #selector(handleContactingNetworkNotification(_:)), name: .contactingNetwork, object: nil)
-        notificationCenter.addObserver(self, selector: #selector(handleSendingMessageNotification(_:)), name: .sendingMessage, object: nil)
+        notificationCenter.addObserver(self, selector: #selector(handleRoutingNotification(_:)), name: .routing, object: nil)
+        notificationCenter.addObserver(self, selector: #selector(handleMessageSendingNotification(_:)), name: .messageSending, object: nil)
         notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
         notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil)
     }
@@ -99,14 +99,14 @@ final class ConversationTitleView : UIView {
         setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
     }
     
-    @objc private func handleContactingNetworkNotification(_ notification: Notification) {
+    @objc private func handleRoutingNotification(_ notification: Notification) {
         guard let timestamp = notification.object as? NSNumber else { return }
-        setStatusIfNeeded(to: .contactingNetwork, forMessageWithTimestamp: timestamp)
+        setStatusIfNeeded(to: .routing, forMessageWithTimestamp: timestamp)
     }
     
-    @objc private func handleSendingMessageNotification(_ notification: Notification) {
+    @objc private func handleMessageSendingNotification(_ notification: Notification) {
         guard let timestamp = notification.object as? NSNumber else { return }
-        setStatusIfNeeded(to: .sendingMessage, forMessageWithTimestamp: timestamp)
+        setStatusIfNeeded(to: .messageSending, forMessageWithTimestamp: timestamp)
     }
     
     @objc private func handleMessageSentNotification(_ notification: Notification) {
@@ -147,8 +147,8 @@ final class ConversationTitleView : UIView {
             self.subtitleLabel.isHidden = false
             switch self.currentStatus {
             case .calculatingPoW: self.subtitleLabel.text = NSLocalizedString("Encrypting message", comment: "")
-            case .contactingNetwork: self.subtitleLabel.text = NSLocalizedString("Tracing a path", comment: "")
-            case .sendingMessage: self.subtitleLabel.text = NSLocalizedString("Sending message", comment: "")
+            case .routing: self.subtitleLabel.text = NSLocalizedString("Tracing a path", comment: "")
+            case .messageSending: self.subtitleLabel.text = NSLocalizedString("Sending message", comment: "")
             case .messageSent: self.subtitleLabel.text = NSLocalizedString("Message sent securely", comment: "")
             case .messageFailed: self.subtitleLabel.text = NSLocalizedString("Message failed to send", comment: "")
             case nil:
diff --git a/Signal/src/Loki/Utilities/LokiPushNotificationManager.swift b/Signal/src/Loki/Utilities/LokiPushNotificationManager.swift
index d11ff6891..9624c4323 100644
--- a/Signal/src/Loki/Utilities/LokiPushNotificationManager.swift
+++ b/Signal/src/Loki/Utilities/LokiPushNotificationManager.swift
@@ -22,7 +22,11 @@ final class LokiPushNotificationManager : NSObject {
         }
         // Send token to Loki server
         let parameters = [ "token" : hexEncodedToken ]
+        #if DEBUG
+        let url = URL(string: "https://dev.apns.getsession.org/register")!
+        #else
         let url = URL(string: "https://live.apns.getsession.org/register")!
+        #endif
         let request = TSRequest(url: url, method: "POST", parameters: parameters)
         request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ]
         TSNetworkManager.shared().makeRequest(request, success: { _, response in
diff --git a/Signal/src/Loki/View Controllers/HomeVC.swift b/Signal/src/Loki/View Controllers/HomeVC.swift
index 7e0baf8ea..73bbff24f 100644
--- a/Signal/src/Loki/View Controllers/HomeVC.swift	
+++ b/Signal/src/Loki/View Controllers/HomeVC.swift	
@@ -134,8 +134,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
         if OWSIdentityManager.shared().identityKeyPair() != nil {
             let appDelegate = UIApplication.shared.delegate as! AppDelegate
             appDelegate.setUpDefaultPublicChatsIfNeeded()
+            appDelegate.startOpenGroupPollersIfNeeded()
             appDelegate.createRSSFeedsIfNeeded()
-            LokiPublicChatManager.shared.startPollersIfNeeded()
             appDelegate.startRSSFeedPollersIfNeeded()
         }
         // Do initial update
diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
index 691ee9e4f..741231a4e 100644
--- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
+++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
@@ -429,12 +429,12 @@ typedef enum : NSUInteger {
                                                  name:NSNotification.calculatingPoW
                                                object:nil];
     [[NSNotificationCenter defaultCenter] addObserver:self
-                                             selector:@selector(handleContactingNetworkNotification:)
-                                                 name:NSNotification.contactingNetwork
+                                             selector:@selector(handleRoutingNotification:)
+                                                 name:NSNotification.routing
                                                object:nil];
     [[NSNotificationCenter defaultCenter] addObserver:self
-                                             selector:@selector(handleSendingMessageNotification:)
-                                                 name:NSNotification.sendingMessage
+                                             selector:@selector(handleMessageSendingNotification:)
+                                                 name:NSNotification.messageSending
                                                object:nil];
     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(handleMessageSentNotification:)
@@ -5419,13 +5419,13 @@ typedef enum : NSUInteger {
     [self setProgressIfNeededTo:0.25f forMessageWithTimestamp:timestamp];
 }
 
-- (void)handleContactingNetworkNotification:(NSNotification *)notification
+- (void)handleRoutingNotification:(NSNotification *)notification
 {
     NSNumber *timestamp = (NSNumber *)notification.object;
     [self setProgressIfNeededTo:0.50f forMessageWithTimestamp:timestamp];
 }
 
-- (void)handleSendingMessageNotification:(NSNotification *)notification
+- (void)handleMessageSendingNotification:(NSNotification *)notification
 {
     NSNumber *timestamp = (NSNumber *)notification.object;
     [self setProgressIfNeededTo:0.75f forMessageWithTimestamp:timestamp];
diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m
index 14404235d..6e65a8e0f 100644
--- a/Signal/src/ViewControllers/HomeView/HomeViewController.m
+++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m
@@ -682,6 +682,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
             [LKAPI clearRandomSnodePool];
             AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
             [appDelegate stopLongPollerIfNeeded];
+            [appDelegate stopOpenGroupPollersIfNeeded];
             [SSKEnvironment.shared.tsAccountManager resetForReregistration];
             UIViewController *rootViewController = [[OnboardingController new] initialViewController];
             OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:rootViewController];
diff --git a/Signal/src/util/MainAppContext.m b/Signal/src/util/MainAppContext.m
index bfb746148..7a7139312 100644
--- a/Signal/src/util/MainAppContext.m
+++ b/Signal/src/util/MainAppContext.m
@@ -28,6 +28,7 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
 
 @synthesize mainWindow = _mainWindow;
 @synthesize appLaunchTime = _appLaunchTime;
+@synthesize wasWokenUpBySilentPushNotification = _wasWokenUpBySilentPushNotification;
 
 - (instancetype)init
 {
@@ -40,6 +41,7 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
     self.reportedApplicationState = UIApplicationStateInactive;
 
     _appLaunchTime = [NSDate new];
+    _wasWokenUpBySilentPushNotification = false;
 
     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(applicationWillEnterForeground:)
diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m
index bd2b6887e..1bbea7d6e 100644
--- a/SignalMessaging/profiles/OWSProfileManager.m
+++ b/SignalMessaging/profiles/OWSProfileManager.m
@@ -416,10 +416,10 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
             NSData *encryptedAvatarData = [self encryptProfileData:avatarData profileKey:newProfileKey];
             OWSAssertDebug(encryptedAvatarData.length > 0);
             
-            [[LKFileServerAPI setProfilePicture:encryptedAvatarData]
-            .thenOn(dispatch_get_main_queue(), ^(NSString *url) {
+            [[LKFileServerAPI uploadProfilePicture:encryptedAvatarData]
+            .thenOn(dispatch_get_main_queue(), ^(NSString *downloadURL) {
                 [self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
-                   successBlock(url);
+                   successBlock(downloadURL);
                 }];
             })
             .catchOn(dispatch_get_main_queue(), ^(id result) {
diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift
index ba3b130de..61ec13fce 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift
@@ -1,7 +1,8 @@
 import PromiseKit
 
 public extension LokiAPI {
-    
+
+    /// Only ever accessed from `LokiAPI.errorHandlingQueue` to avoid race conditions.
     fileprivate static var failureCount: [LokiAPITarget:UInt] = [:]
     
     // MARK: Settings
diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift
index 54aad067a..126eac01b 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift
@@ -187,10 +187,10 @@ public final class LokiAPI : NSObject {
         func sendLokiMessageUsingSwarmAPI() -> Promise<Set<RawResponsePromise>> {
             notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
             return lokiMessage.calculatePoW().then { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
-                notificationCenter.post(name: .contactingNetwork, object: NSNumber(value: signalMessage.timestamp))
+                notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
                 return getTargetSnodes(for: destination).map { swarm in
                     return Set(swarm.map { target in
-                        notificationCenter.post(name: .sendingMessage, object: NSNumber(value: signalMessage.timestamp))
+                        notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
                         return sendLokiMessage(lokiMessageWithPoW, to: target).map { rawResponse in
                             if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
                                 guard powDifficulty != LokiAPI.powDifficulty else { return rawResponse }
diff --git a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift
index 7dcac5e9f..586e10069 100644
--- a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift
@@ -137,40 +137,33 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
     }
     
     // MARK: Profile Pictures (Public API)
-    public static func setProfilePicture(_ profilePicture: Data) -> Promise<String> {
-        return Promise<String>() { seal in
-            guard profilePicture.count < maxFileSize else { return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded) }
-            getAuthToken(for: server).done { token in
-                let url = "\(server)/users/me/avatar"
-                let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
-                var error: NSError?
-                var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
-                    formData.appendPart(withFileData: profilePicture, name: "avatar", fileName: UUID().uuidString, mimeType: "application/binary")
-                }, error: &error)
-                request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
-                if let error = error {
-                    print("[Loki] Couldn't upload profile picture due to error: \(error).")
-                    throw error
-                }
-                let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done { responseObject in
-                    guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let profilePicture = data["avatar_image"] as? JSON, let downloadURL = profilePicture["url"] as? String else {
-                        print("[Loki] Couldn't parse profile picture from: \(responseObject).")
-                        return seal.reject(LokiDotNetAPIError.parsingFailed)
-                    }
-                    return seal.fulfill(downloadURL)
-                }.catch { error in
-                    seal.reject(error)
-                }
-            }.catch { error in
-                print("[Loki] Couldn't upload profile picture due to error: \(error).")
-                seal.reject(error)
+    public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
+        guard profilePicture.count < maxFileSize else { return Promise(error: LokiDotNetAPIError.maxFileSizeExceeded) }
+        let url = "\(server)/files"
+        let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
+        var error: NSError?
+        var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
+            formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary")
+        }, error: &error)
+        // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
+        request.addValue("Bearer loki", forHTTPHeaderField: "Authorization")
+        if let error = error {
+            print("[Loki] Couldn't upload profile picture due to error: \(error).")
+            return Promise(error: error)
+        }
+        return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map { responseObject in
+            guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
+                print("[Loki] Couldn't parse profile picture from: \(responseObject).")
+                throw LokiDotNetAPIError.parsingFailed
             }
+            UserDefaults.standard[.lastProfilePictureUpload] = Date()
+            return downloadURL
         }
     }
     
     // MARK: Profile Pictures (Public Obj-C API)
-    @objc(setProfilePicture:)
-    public static func objc_setProfilePicture(_ profilePicture: Data) -> AnyPromise {
-        return AnyPromise.from(setProfilePicture(profilePicture))
+    @objc(uploadProfilePicture:)
+    public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise {
+        return AnyPromise.from(uploadProfilePicture(profilePicture))
     }
 }
diff --git a/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift b/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
index f1d426fd7..84921c642 100644
--- a/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
+++ b/SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
@@ -9,7 +9,11 @@ public enum LKUserDefaults {
         /// Whether the device was unlinked as a slave device (used to notify the user on the landing screen).
         case wasUnlinked
     }
-    
+
+    public enum Date : Swift.String {
+        case lastProfilePictureUpload
+    }
+
     public enum Double : Swift.String {
         case lastDeviceTokenUpload = "lastDeviceTokenUploadTime"
     }
@@ -36,6 +40,11 @@ public extension UserDefaults {
         get { return self.bool(forKey: bool.rawValue) }
         set { set(newValue, forKey: bool.rawValue) }
     }
+
+    public subscript(date: LKUserDefaults.Date) -> Date? {
+        get { return self.object(forKey: date.rawValue) as? Date }
+        set { set(newValue, forKey: date.rawValue) }
+    }
     
     public subscript(double: LKUserDefaults.Double) -> Double {
         get { return self.double(forKey: double.rawValue) }
diff --git a/SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift b/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift
similarity index 78%
rename from SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift
rename to SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift
index 176c3adef..f4ab9bef8 100644
--- a/SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift
+++ b/SignalServiceKit/src/Loki/Utilities/Notification+Loki.swift
@@ -1,36 +1,40 @@
 
 public extension Notification.Name {
+
+    // State changes
     public static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged")
-    public static let newMessagesReceived = Notification.Name("newMessagesReceived")
     public static let threadFriendRequestStatusChanged = Notification.Name("threadFriendRequestStatusChanged")
     public static let messageFriendRequestStatusChanged = Notification.Name("messageFriendRequestStatusChanged")
     public static let threadDeleted = Notification.Name("threadDeleted")
-    public static let dataNukeRequested = Notification.Name("dataNukeRequested")
     public static let threadSessionRestoreDevicesChanged = Notification.Name("threadSessionRestoreDevicesChanged")
-    // Message statuses
+    // Message status changes
     public static let calculatingPoW = Notification.Name("calculatingPoW")
-    public static let contactingNetwork = Notification.Name("contactingNetwork")
-    public static let sendingMessage = Notification.Name("sendingMessage")
+    public static let routing = Notification.Name("routing")
+    public static let messageSending = Notification.Name("messageSending")
     public static let messageSent = Notification.Name("messageSent")
     public static let messageFailed = Notification.Name("messageFailed")
     // Onboarding
     public static let seedViewed = Notification.Name("seedViewed")
+    // Interaction
+    public static let dataNukeRequested = Notification.Name("dataNukeRequested")
 }
 
 @objc public extension NSNotification {
+
+    // State changes
     @objc public static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString
-    @objc public static let newMessagesReceived = Notification.Name.newMessagesReceived.rawValue as NSString
     @objc public static let threadFriendRequestStatusChanged = Notification.Name.threadFriendRequestStatusChanged.rawValue as NSString
     @objc public static let messageFriendRequestStatusChanged = Notification.Name.messageFriendRequestStatusChanged.rawValue as NSString
     @objc public static let threadDeleted = Notification.Name.threadDeleted.rawValue as NSString
-    @objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
     @objc public static let threadSessionRestoreDevicesChanged = Notification.Name.threadSessionRestoreDevicesChanged.rawValue as NSString
     // Message statuses
     @objc public static let calculatingPoW = Notification.Name.calculatingPoW.rawValue as NSString
-    @objc public static let contactingNetwork = Notification.Name.contactingNetwork.rawValue as NSString
-    @objc public static let sendingMessage = Notification.Name.sendingMessage.rawValue as NSString
+    @objc public static let routing = Notification.Name.routing.rawValue as NSString
+    @objc public static let messageSending = Notification.Name.messageSending.rawValue as NSString
     @objc public static let messageSent = Notification.Name.messageSent.rawValue as NSString
     @objc public static let messageFailed = Notification.Name.messageFailed.rawValue as NSString
     // Onboarding
     @objc public static let seedViewed = Notification.Name.seedViewed.rawValue as NSString
+    // Interaction
+    @objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
 }
diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h
index 1701e9296..74b9553f8 100644
--- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h
+++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h
@@ -43,6 +43,8 @@ extern NSString *const kAttachmentDownloadAttachmentIDKey;
                           success:(void (^)(NSArray<TSAttachmentStream *> *attachmentStreams))success
                           failure:(void (^)(NSError *error))failure;
 
+- (void)continueDownloadIfPossible;
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m
index 4db343ed9..f98e71815 100644
--- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m
+++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m
@@ -264,6 +264,8 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
 
 - (void)startDownloadIfPossible
 {
+    if (CurrentAppContext().wasWokenUpBySilentPushNotification) { return; }
+    
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         OWSAttachmentDownloadJob *_Nullable job;
 
@@ -342,6 +344,16 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
 
 #pragma mark -
 
+- (void)continueDownloadIfPossible
+{
+    if (self.attachmentDownloadJobQueue.count > 0) {
+        [LKLogger print:@"[Loki] Continuing unfinished attachment download tasks."];
+        [self startDownloadIfPossible];
+    }
+}
+
+#pragma mark -
+
 - (void)retrieveAttachmentForJob:(OWSAttachmentDownloadJob *)job
                          success:(void (^)(TSAttachmentStream *attachmentStream))successHandler
                          failure:(void (^)(NSError *error))failureHandler
diff --git a/SignalServiceKit/src/Util/AppContext.h b/SignalServiceKit/src/Util/AppContext.h
index bcb8cde8d..9a0fd9d4d 100755
--- a/SignalServiceKit/src/Util/AppContext.h
+++ b/SignalServiceKit/src/Util/AppContext.h
@@ -37,6 +37,9 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
 
 @property (nonatomic, readonly) BOOL isMainApp;
 @property (nonatomic, readonly) BOOL isMainAppAndActive;
+/// Whether the app was woken up by a silent push notification. This is important for
+/// determining whether attachments should be downloaded or not.
+@property (nonatomic) BOOL wasWokenUpBySilentPushNotification;
 
 // Whether the user is using a right-to-left language like Arabic.
 @property (nonatomic, readonly) BOOL isRTL;