From 6d6d45b2833716e39c43c2a7d4054bf5baa5e14b Mon Sep 17 00:00:00 2001
From: Morgan Pretty <morgan.t.pretty@gmail.com>
Date: Mon, 15 Aug 2022 10:35:52 +1000
Subject: [PATCH 1/2] Updated the ProfilePictureView to only use YYImage for
 Gif and WebP images

Added support for animated OpenGroup images
---
 Session/Shared/FullConversationCell.swift     |   6 +-
 .../SimplifiedConversationCell.swift          |   2 +-
 SessionUtilitiesKit/Media/Data+Image.swift    |   5 +
 SessionUtilitiesKit/Media/ImageFormat.swift   |   1 +
 SessionUtilitiesKit/Media/NSData+Image.m      |   2 +-
 .../Profile Pictures/ProfilePictureView.swift | 191 +++++++++++++-----
 6 files changed, 155 insertions(+), 52 deletions(-)

diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift
index ebf55e4c2..14faf6618 100644
--- a/Session/Shared/FullConversationCell.swift
+++ b/Session/Shared/FullConversationCell.swift
@@ -240,7 +240,7 @@ public final class FullConversationCell: UITableViewCell {
             profile: cellViewModel.profile,
             additionalProfile: cellViewModel.additionalProfile,
             threadVariant: cellViewModel.threadVariant,
-            openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
+            openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
             useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
         )
         
@@ -280,7 +280,7 @@ public final class FullConversationCell: UITableViewCell {
             profile: cellViewModel.profile,
             additionalProfile: cellViewModel.additionalProfile,
             threadVariant: cellViewModel.threadVariant,
-            openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
+            openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
             useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
         )
         
@@ -341,7 +341,7 @@ public final class FullConversationCell: UITableViewCell {
             profile: cellViewModel.profile,
             additionalProfile: cellViewModel.additionalProfile,
             threadVariant: cellViewModel.threadVariant,
-            openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
+            openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
             useFallbackPicture: (
                 cellViewModel.threadVariant == .openGroup &&
                 cellViewModel.openGroupProfilePictureData == nil
diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift
index 9bea5687a..852379361 100644
--- a/SessionShareExtension/SimplifiedConversationCell.swift
+++ b/SessionShareExtension/SimplifiedConversationCell.swift
@@ -94,7 +94,7 @@ final class SimplifiedConversationCell: UITableViewCell {
             profile: cellViewModel.profile,
             additionalProfile: cellViewModel.additionalProfile,
             threadVariant: cellViewModel.threadVariant,
-            openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
+            openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
             useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil),
             showMultiAvatarForClosedGroup: true
         )
diff --git a/SessionUtilitiesKit/Media/Data+Image.swift b/SessionUtilitiesKit/Media/Data+Image.swift
index 57adb8a4d..429ed9d5f 100644
--- a/SessionUtilitiesKit/Media/Data+Image.swift
+++ b/SessionUtilitiesKit/Media/Data+Image.swift
@@ -34,6 +34,8 @@ public extension Data {
             case (0x42, 0x4d): return .bmp
             case (0x4D, 0x4D): return .tiff // Motorola byte order TIFF
             case (0x49, 0x49): return .tiff // Intel byte order TIFF
+            case (0x52, 0x49): return .webp // First two letters of WebP
+                
             default: return .unknown
         }
     }
@@ -113,6 +115,9 @@ public extension Data {
                     mimeType == OWSMimeTypeImageBmp1 ||
                     mimeType == OWSMimeTypeImageBmp2
                 )
+                
+            case .webp:
+                return (mimeType == nil || mimeType == OWSMimeTypeImageWebp)
         }
     }
     
diff --git a/SessionUtilitiesKit/Media/ImageFormat.swift b/SessionUtilitiesKit/Media/ImageFormat.swift
index e31f408c8..1db97f1df 100644
--- a/SessionUtilitiesKit/Media/ImageFormat.swift
+++ b/SessionUtilitiesKit/Media/ImageFormat.swift
@@ -9,4 +9,5 @@ public enum ImageFormat {
     case tiff
     case jpeg
     case bmp
+    case webp
 }
diff --git a/SessionUtilitiesKit/Media/NSData+Image.m b/SessionUtilitiesKit/Media/NSData+Image.m
index 5af4610cd..fb13b9d20 100644
--- a/SessionUtilitiesKit/Media/NSData+Image.m
+++ b/SessionUtilitiesKit/Media/NSData+Image.m
@@ -328,7 +328,7 @@ typedef struct {
         // Intel byte order TIFF
         return ImageFormat_Tiff;
     } else if (byte0 == 0x52 && byte1 == 0x49) {
-        // First two letters of RIFF tag.
+        // First two letters of WebP tag.
         return ImageFormat_Webp;
     }
 
diff --git a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift
index 5dd308a11..bb8b477b1 100644
--- a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift	
+++ b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift	
@@ -19,8 +19,61 @@ public final class ProfilePictureView: UIView {
     
     // MARK: - Components
     
-    private lazy var imageView = getImageView()
-    private lazy var additionalImageView = getImageView()
+    private lazy var imageContainerView: UIView = {
+        let result: UIView = UIView()
+        result.translatesAutoresizingMaskIntoConstraints = false
+        result.clipsToBounds = true
+        result.backgroundColor = Colors.unimportant
+        
+        return result
+    }()
+    
+    private lazy var imageView: UIImageView = {
+        let result: UIImageView = UIImageView()
+        result.translatesAutoresizingMaskIntoConstraints = false
+        result.contentMode = .scaleAspectFill
+        result.isHidden = true
+        
+        return result
+    }()
+    
+    private lazy var animatedImageView: YYAnimatedImageView = {
+        let result: YYAnimatedImageView = YYAnimatedImageView()
+        result.translatesAutoresizingMaskIntoConstraints = false
+        result.contentMode = .scaleAspectFill
+        result.isHidden = true
+        
+        return result
+    }()
+    
+    private lazy var additionalImageContainerView: UIView = {
+        let result: UIView = UIView()
+        result.translatesAutoresizingMaskIntoConstraints = false
+        result.clipsToBounds = true
+        result.backgroundColor = Colors.unimportant
+        result.layer.cornerRadius = (Values.smallProfilePictureSize / 2)
+        result.isHidden = true
+        
+        return result
+    }()
+    
+    private lazy var additionalImageView: UIImageView = {
+        let result: UIImageView = UIImageView()
+        result.translatesAutoresizingMaskIntoConstraints = false
+        result.contentMode = .scaleAspectFill
+        result.isHidden = true
+        
+        return result
+    }()
+    
+    private lazy var additionalAnimatedImageView: YYAnimatedImageView = {
+        let result: YYAnimatedImageView = YYAnimatedImageView()
+        result.translatesAutoresizingMaskIntoConstraints = false
+        result.contentMode = .scaleAspectFill
+        result.isHidden = true
+        
+        return result
+    }()
     
     // MARK: - Lifecycle
     
@@ -35,27 +88,33 @@ public final class ProfilePictureView: UIView {
     }
     
     private func setUpViewHierarchy() {
-        // Set up image view
-        addSubview(imageView)
-        imageView.pin(.leading, to: .leading, of: self)
-        imageView.pin(.top, to: .top, of: self)
-        
         let imageViewSize = CGFloat(Values.mediumProfilePictureSize)
-        imageViewWidthConstraint = imageView.set(.width, to: imageViewSize)
-        imageViewHeightConstraint = imageView.set(.height, to: imageViewSize)
+        let additionalImageViewSize = CGFloat(Values.smallProfilePictureSize)
         
-        // Set up additional image view
-        addSubview(additionalImageView)
-        additionalImageView.pin(.trailing, to: .trailing, of: self)
-        additionalImageView.pin(.bottom, to: .bottom, of: self)
+        addSubview(imageContainerView)
+        addSubview(additionalImageContainerView)
         
-        let additionalImageViewSize = CGFloat(Values.smallProfilePictureSize)
-        additionalImageViewWidthConstraint = additionalImageView.set(.width, to: additionalImageViewSize)
-        additionalImageViewHeightConstraint = additionalImageView.set(.height, to: additionalImageViewSize)
-        additionalImageView.layer.cornerRadius = additionalImageViewSize / 2
+        imageContainerView.pin(.leading, to: .leading, of: self)
+        imageContainerView.pin(.top, to: .top, of: self)
+        imageViewWidthConstraint = imageContainerView.set(.width, to: imageViewSize)
+        imageViewHeightConstraint = imageContainerView.set(.height, to: imageViewSize)
+        additionalImageContainerView.pin(.trailing, to: .trailing, of: self)
+        additionalImageContainerView.pin(.bottom, to: .bottom, of: self)
+        additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: additionalImageViewSize)
+        additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: additionalImageViewSize)
+        
+        imageContainerView.addSubview(imageView)
+        imageContainerView.addSubview(animatedImageView)
+        additionalImageContainerView.addSubview(additionalImageView)
+        additionalImageContainerView.addSubview(additionalAnimatedImageView)
+        
+        imageView.pin(to: imageContainerView)
+        animatedImageView.pin(to: imageContainerView)
+        additionalImageView.pin(to: additionalImageContainerView)
+        additionalAnimatedImageView.pin(to: additionalImageContainerView)
     }
     
-    // FIXME: Remove this once we refactor the ConversationVC to Swift (use the HomeViewModel approach)
+    // FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach)
     @objc(updateForThreadId:)
     public func update(forThreadId threadId: String?) {
         guard
@@ -74,7 +133,7 @@ public final class ProfilePictureView: UIView {
             profile: viewModel.profile,
             additionalProfile: viewModel.additionalProfile,
             threadVariant: viewModel.threadVariant,
-            openGroupProfilePicture: viewModel.openGroupProfilePictureData.map { UIImage(data: $0) },
+            openGroupProfilePictureData: viewModel.openGroupProfilePictureData,
             useFallbackPicture: (
                 viewModel.threadVariant == .openGroup &&
                 viewModel.openGroupProfilePictureData == nil
@@ -88,7 +147,7 @@ public final class ProfilePictureView: UIView {
         profile: Profile? = nil,
         additionalProfile: Profile? = nil,
         threadVariant: SessionThread.Variant,
-        openGroupProfilePicture: UIImage? = nil,
+        openGroupProfilePictureData: Data? = nil,
         useFallbackPicture: Bool = false,
         showMultiAvatarForClosedGroup: Bool = false
     ) {
@@ -101,20 +160,38 @@ public final class ProfilePictureView: UIView {
             }
             
             imageView.contentMode = .center
-            imageView.backgroundColor = UIColor(rgbHex: 0x353535)
-            imageView.layer.cornerRadius = (self.size / 2)
+            imageView.isHidden = false
+            animatedImageView.isHidden = true
+            imageContainerView.backgroundColor = UIColor(rgbHex: 0x353535)
+            imageContainerView.layer.cornerRadius = (self.size / 2)
             imageViewWidthConstraint.constant = self.size
             imageViewHeightConstraint.constant = self.size
-            additionalImageView.isHidden = true
+            additionalImageContainerView.isHidden = true
+            animatedImageView.image = nil
             additionalImageView.image = nil
-            additionalImageView.layer.cornerRadius = (self.size / 2)
+            additionalAnimatedImageView.image = nil
+            additionalImageView.isHidden = true
+            additionalAnimatedImageView.isHidden = true
             return
         }
-        guard !publicKey.isEmpty || openGroupProfilePicture != nil else { return }
+        guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return }
         
-        func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage, isTappable: Bool) {
-            if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile), let image: YYImage = YYImage(data: profileData) {
-                return (image, true)
+        func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage?, animatedImage: YYImage?, isTappable: Bool) {
+            if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) {
+                let format: ImageFormat = profileData.guessedImageFormat
+                
+                let image: UIImage? = (format == .gif || format == .webp ?
+                    nil :
+                    UIImage(data: profileData)
+                )
+                let animatedImage: YYImage? = (format != .gif && format != .webp ?
+                    nil :
+                    YYImage(data: profileData)
+                )
+                
+                if image != nil || animatedImage != nil {
+                    return (image, animatedImage, true)
+                }
             }
             
             return (
@@ -124,6 +201,7 @@ public final class ProfilePictureView: UIView {
                         .defaulting(to: publicKey),
                     size: size
                 ),
+                nil,
                 false
             )
         }
@@ -147,56 +225,75 @@ public final class ProfilePictureView: UIView {
                 imageViewHeightConstraint.constant = targetSize
                 additionalImageViewWidthConstraint.constant = targetSize
                 additionalImageViewHeightConstraint.constant = targetSize
-                additionalImageView.isHidden = false
+                additionalImageContainerView.isHidden = false
                 
                 if let additionalProfile: Profile = additionalProfile {
-                    additionalImageView.image = getProfilePicture(
+                    let (image, animatedImage, _): (UIImage?, YYImage?, Bool) = getProfilePicture(
                         of: targetSize,
                         for: additionalProfile.id,
                         profile: additionalProfile
-                    ).image
+                    )
+                    
+                    // Set the images and show the appropriate imageView (non-animated should be
+                    // visible if there is no image)
+                    additionalImageView.image = image
+                    additionalAnimatedImageView.image = animatedImage
+                    additionalImageView.isHidden = (animatedImage != nil)
+                    additionalAnimatedImageView.isHidden = (animatedImage == nil)
                 }
                 
             default:
                 targetSize = self.size
                 imageViewWidthConstraint.constant = targetSize
                 imageViewHeightConstraint.constant = targetSize
-                additionalImageView.isHidden = true
+                additionalImageContainerView.isHidden = true
                 additionalImageView.image = nil
+                additionalImageView.isHidden = true
+                additionalAnimatedImageView.image = nil
+                additionalAnimatedImageView.isHidden = true
         }
         
         // Set the image
-        if let openGroupProfilePicture: UIImage = openGroupProfilePicture {
-            imageView.image = openGroupProfilePicture
+        if let openGroupProfilePictureData: Data = openGroupProfilePictureData {
+            let format: ImageFormat = openGroupProfilePictureData.guessedImageFormat
+            
+            let image: UIImage? = (format == .gif || format == .webp ?
+                nil :
+                UIImage(data: openGroupProfilePictureData)
+            )
+            let animatedImage: YYImage? = (format != .gif && format != .webp ?
+                nil :
+                YYImage(data: openGroupProfilePictureData)
+            )
+            
+            imageView.image = image
+            animatedImageView.image = animatedImage
+            imageView.isHidden = (animatedImage != nil)
+            animatedImageView.isHidden = (animatedImage == nil)
             hasTappableProfilePicture = true
         }
         else {
-            let (image, isTappable): (UIImage, Bool) = getProfilePicture(
+            let (image, animatedImage, isTappable): (UIImage?, YYImage?, Bool) = getProfilePicture(
                 of: targetSize,
                 for: publicKey,
                 profile: profile
             )
             imageView.image = image
+            animatedImageView.image = animatedImage
+            imageView.isHidden = (animatedImage != nil)
+            animatedImageView.isHidden = (animatedImage == nil)
             hasTappableProfilePicture = isTappable
         }
         
         imageView.contentMode = .scaleAspectFill
-        imageView.backgroundColor = Colors.unimportant
-        imageView.layer.cornerRadius = (targetSize / 2)
-        additionalImageView.layer.cornerRadius = (targetSize / 2)
+        animatedImageView.contentMode = .scaleAspectFill
+        imageContainerView.backgroundColor = Colors.unimportant
+        imageContainerView.layer.cornerRadius = (targetSize / 2)
+        additionalImageContainerView.layer.cornerRadius = (targetSize / 2)
     }
     
     // MARK: - Convenience
     
-    private func getImageView() -> YYAnimatedImageView {
-        let result = YYAnimatedImageView()
-        result.layer.masksToBounds = true
-        result.backgroundColor = Colors.unimportant
-        result.contentMode = .scaleAspectFill
-        
-        return result
-    }
-    
     @objc public func getProfilePicture() -> UIImage? {
         return (hasTappableProfilePicture ? imageView.image : nil)
     }

From 3ab8bdec7726fecb52aa12eda2859d9dea8ddc62 Mon Sep 17 00:00:00 2001
From: Morgan Pretty <morgan.t.pretty@gmail.com>
Date: Tue, 16 Aug 2022 13:56:40 +1000
Subject: [PATCH 2/2] Fixed an issue where hidden mods/admins wouldn't be
 properly recognised

Added an isHidden flag to the GroupMember
Reset the OpenGroup 'infoUpdates' value to force a re-fetch of the data and fix users affected by this bug
Fixed the broken unit tests
---
 Podfile                                       |   3 +
 Podfile.lock                                  |   2 +-
 Session.xcodeproj/project.pbxproj             |  28 ++
 Session/Utilities/MockDataGenerator.swift     |   6 +-
 SessionMessagingKit/Configuration.swift       |   3 +-
 .../Migrations/_003_YDBToGRDBMigration.swift  |   9 +-
 .../_006_FixHiddenModAdminSupport.swift       |  30 ++
 .../Database/Models/GroupMember.swift         |   6 +-
 .../Open Groups/OpenGroupManager.swift        |  28 +-
 .../MessageReceiver+ClosedGroups.swift        |  12 +-
 .../MessageSender+ClosedGroups.swift          |   9 +-
 .../Open Groups/Models/CapabilitiesSpec.swift |  16 +-
 .../Open Groups/Models/OpenGroupSpec.swift    |   2 +-
 .../Open Groups/OpenGroupAPISpec.swift        |   2 +
 .../Open Groups/OpenGroupManagerSpec.swift    | 276 ++++++++++++++++--
 .../Database/Models/Identity.swift            |  16 +-
 .../Database/Types/TypedTableAlteration.swift |  26 ++
 .../Utilities/Database+Utilities.swift        |  11 +
 .../Database/Models/IdentitySpec.swift        | 105 +++++++
 19 files changed, 539 insertions(+), 51 deletions(-)
 create mode 100644 SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift
 create mode 100644 SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift
 create mode 100644 SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift

diff --git a/Podfile b/Podfile
index ae568f035..7e448b202 100644
--- a/Podfile
+++ b/Podfile
@@ -66,6 +66,9 @@ abstract_target 'GlobalDependencies' do
           
           pod 'Quick'
           pod 'Nimble'
+          
+          # Need to include this for the tests because otherwise it won't actually build
+          pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage'
         end
       end
       
diff --git a/Podfile.lock b/Podfile.lock
index 37f4ac9c5..1315eb501 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -242,6 +242,6 @@ SPEC CHECKSUMS:
   YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
   ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
 
-PODFILE CHECKSUM: f0857369c4831b2e5c1946345e76e493f3286805
+PODFILE CHECKSUM: 0e694576fbda3c10bbc762998183d97142b85896
 
 COCOAPODS: 1.11.3
diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj
index c568bb312..e77691409 100644
--- a/Session.xcodeproj/project.pbxproj
+++ b/Session.xcodeproj/project.pbxproj
@@ -645,6 +645,9 @@
 		FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
 		FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
 		FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; };
+		FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; };
+		FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; };
+		FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; };
 		FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
 		FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
 		FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
@@ -1685,6 +1688,9 @@
 		FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = "<group>"; };
 		FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
 		FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = "<group>"; };
+		FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = "<group>"; };
+		FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = "<group>"; };
+		FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = "<group>"; };
 		FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
 		FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = "<group>"; };
 		FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
@@ -3457,6 +3463,7 @@
 				FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */,
 				FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */,
 				FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */,
+				FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */,
 			);
 			path = Migrations;
 			sourceTree = "<group>";
@@ -3523,6 +3530,7 @@
 				FD17D7B727F51ECA00122BE0 /* Migration.swift */,
 				FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
 				FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
+				FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */,
 				FD7162DA281B6C440060647B /* TypedTableAlias.swift */,
 				FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */,
 			);
@@ -3570,6 +3578,22 @@
 			path = LegacyDatabase;
 			sourceTree = "<group>";
 		};
+		FD37EA1228AB3F60003AE748 /* Database */ = {
+			isa = PBXGroup;
+			children = (
+				FD37EA1328AB42C1003AE748 /* Models */,
+			);
+			path = Database;
+			sourceTree = "<group>";
+		};
+		FD37EA1328AB42C1003AE748 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
 		FD3C905D27E410DB00CD579F /* Common Networking */ = {
 			isa = PBXGroup;
 			children = (
@@ -3659,6 +3683,7 @@
 		FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */ = {
 			isa = PBXGroup;
 			children = (
+				FD37EA1228AB3F60003AE748 /* Database */,
 				FD83B9B927CF20A5005E1583 /* General */,
 			);
 			path = SessionUtilitiesKitTests;
@@ -5018,6 +5043,7 @@
 				FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
 				C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
 				C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */,
+				FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */,
 				C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
 				C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
 				C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,
@@ -5125,6 +5151,7 @@
 				7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */,
 				FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */,
 				FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */,
+				FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */,
 				C3D9E3BF25676AD70040E4F3 /* (null) in Sources */,
 				B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */,
 				FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */,
@@ -5444,6 +5471,7 @@
 				FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
 				FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
 				FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
+				FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
 				FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift
index 9b0b12090..457a50b89 100644
--- a/Session/Utilities/MockDataGenerator.swift
+++ b/Session/Utilities/MockDataGenerator.swift
@@ -256,7 +256,8 @@ enum MockDataGenerator {
                     _ = try! GroupMember(
                         groupId: randomGroupPublicKey,
                         profileId: memberId,
-                        role: .standard
+                        role: .standard,
+                        isHidden: false
                     )
                     .saved(db)
                 }
@@ -264,7 +265,8 @@ enum MockDataGenerator {
                     _ = try! GroupMember(
                         groupId: randomGroupPublicKey,
                         profileId: adminId,
-                        role: .admin
+                        role: .admin,
+                        isHidden: false
                     )
                     .saved(db)
                 }
diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift
index 928133641..2230a04de 100644
--- a/SessionMessagingKit/Configuration.swift
+++ b/SessionMessagingKit/Configuration.swift
@@ -17,7 +17,8 @@ public enum SNMessagingKit { // Just to make the external API nice
                     _004_RemoveLegacyYDB.self
                 ],
                 [
-                    _005_FixDeletedMessageReadState.self
+                    _005_FixDeletedMessageReadState.self,
+                    _006_FixHiddenModAdminSupport.self
                 ]
             ]
         )
diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift
index e1c8d8b4e..b748da16d 100644
--- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift
+++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift
@@ -650,7 +650,8 @@ enum _003_YDBToGRDBMigration: Migration {
                         try GroupMember(
                             groupId: threadId,
                             profileId: memberId,
-                            role: .standard
+                            role: .standard,
+                            isHidden: false
                         ).insert(db)
                         
                         if !validProfileIds.contains(memberId) {
@@ -662,7 +663,8 @@ enum _003_YDBToGRDBMigration: Migration {
                         try GroupMember(
                             groupId: threadId,
                             profileId: adminId,
-                            role: .admin
+                            role: .admin,
+                            isHidden: false
                         ).insert(db)
                         
                         if !validProfileIds.contains(adminId) {
@@ -674,7 +676,8 @@ enum _003_YDBToGRDBMigration: Migration {
                         try GroupMember(
                             groupId: threadId,
                             profileId: zombieId,
-                            role: .zombie
+                            role: .zombie,
+                            isHidden: false
                         ).insert(db)
                         
                         if !validProfileIds.contains(zombieId) {
diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift
new file mode 100644
index 000000000..c1097eb94
--- /dev/null
+++ b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift
@@ -0,0 +1,30 @@
+// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
+
+import Foundation
+import GRDB
+import SessionUtilitiesKit
+
+/// This migration fixes an issue where hidden mods/admins weren't getting recognised as mods/admins, it reset's the `info_updates`
+/// for open groups so they will fully re-fetch their mod/admin lists
+enum _006_FixHiddenModAdminSupport: Migration {
+    static let target: TargetMigrations.Identifier = .messagingKit
+    static let identifier: String = "FixHiddenModAdminSupport"
+    static let needsConfigSync: Bool = false
+    static let minExpectedRunDuration: TimeInterval = 0.1
+    
+    static func migrate(_ db: Database) throws {
+        try db.alter(table: GroupMember.self) { t in
+            t.add(.isHidden, .boolean)
+                .notNull()
+                .defaults(to: false)
+        }
+        
+        // When modifying OpenGroup behaviours we should always look to reset the `infoUpdates`
+        // value for all OpenGroups to ensure they all have the correct state for newly
+        // added/changed fields
+        _ = try OpenGroup
+            .updateAll(db, OpenGroup.Columns.infoUpdates.set(to: 0))
+        
+        Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
+    }
+}
diff --git a/SessionMessagingKit/Database/Models/GroupMember.swift b/SessionMessagingKit/Database/Models/GroupMember.swift
index a59bfc417..4cfe0abd4 100644
--- a/SessionMessagingKit/Database/Models/GroupMember.swift
+++ b/SessionMessagingKit/Database/Models/GroupMember.swift
@@ -17,6 +17,7 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor
         case groupId
         case profileId
         case role
+        case isHidden
     }
     
     public enum Role: Int, Codable, DatabaseValueConvertible {
@@ -29,6 +30,7 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor
     public let groupId: String
     public let profileId: String
     public let role: Role
+    public let isHidden: Bool
     
     // MARK: - Relationships
     
@@ -49,11 +51,13 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor
     public init(
         groupId: String,
         profileId: String,
-        role: Role
+        role: Role,
+        isHidden: Bool
     ) {
         self.groupId = groupId
         self.profileId = profileId
         self.role = role
+        self.isHidden = isHidden
     }
 }
 
diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift
index f1c1c6fcf..3e8370ddb 100644
--- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift	
+++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift	
@@ -422,17 +422,41 @@ public final class OpenGroupManager: NSObject {
                 _ = try GroupMember(
                     groupId: threadId,
                     profileId: adminId,
-                    role: .admin
+                    role: .admin,
+                    isHidden: false
                 ).saved(db)
             }
             
+            try roomDetails.hiddenAdmins
+                .defaulting(to: [])
+                .forEach { adminId in
+                    _ = try GroupMember(
+                        groupId: threadId,
+                        profileId: adminId,
+                        role: .admin,
+                        isHidden: true
+                    ).saved(db)
+                }
+            
             try roomDetails.moderators.forEach { moderatorId in
                 _ = try GroupMember(
                     groupId: threadId,
                     profileId: moderatorId,
-                    role: .moderator
+                    role: .moderator,
+                    isHidden: false
                 ).saved(db)
             }
+            
+            try roomDetails.hiddenModerators
+                .defaulting(to: [])
+                .forEach { moderatorId in
+                    _ = try GroupMember(
+                        groupId: threadId,
+                        profileId: moderatorId,
+                        role: .moderator,
+                        isHidden: true
+                    ).saved(db)
+                }
         }
         
         db.afterNextTransactionCommit { db in
diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift
index e2312383d..20d52e23f 100644
--- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift	
+++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift	
@@ -89,7 +89,8 @@ extension MessageReceiver {
                 try GroupMember(
                     groupId: groupPublicKey,
                     profileId: memberId,
-                    role: .standard
+                    role: .standard,
+                    isHidden: false
                 ).save(db)
             }
             
@@ -97,7 +98,8 @@ extension MessageReceiver {
                 try GroupMember(
                     groupId: groupPublicKey,
                     profileId: adminId,
-                    role: .admin
+                    role: .admin,
+                    isHidden: false
                 ).save(db)
             }
             
@@ -254,7 +256,8 @@ extension MessageReceiver {
                     try GroupMember(
                         groupId: id,
                         profileId: memberId,
-                        role: .standard
+                        role: .standard,
+                        isHidden: false
                     ).insert(db)
                 }
             
@@ -440,7 +443,8 @@ extension MessageReceiver {
                 try GroupMember(
                     groupId: id,
                     profileId: sender,
-                    role: .zombie
+                    role: .zombie,
+                    isHidden: false
                 ).insert(db)
             }
             
diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift
index ff813332e..bc8d52a0b 100644
--- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift	
+++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift	
@@ -37,7 +37,8 @@ extension MessageSender {
             try GroupMember(
                 groupId: groupPublicKey,
                 profileId: adminId,
-                role: .admin
+                role: .admin,
+                isHidden: false
             ).insert(db)
         }
         
@@ -48,7 +49,8 @@ extension MessageSender {
             try GroupMember(
                 groupId: groupPublicKey,
                 profileId: memberId,
-                role: .standard
+                role: .standard,
+                isHidden: false
             ).insert(db)
         }
         
@@ -374,7 +376,8 @@ extension MessageSender {
             try GroupMember(
                 groupId: closedGroup.id,
                 profileId: member,
-                role: .standard
+                role: .standard,
+                isHidden: false
             ).insert(db)
         }
     }
diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift
index 707f8852d..220c9bad2 100644
--- a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift	
@@ -37,7 +37,7 @@ class CapabilitiesSpec: QuickSpec {
         describe("a Capability") {
             context("when initializing") {
                 it("succeeeds with a valid case") {
-                    let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability(
+                    let capability: Capability.Variant = Capability.Variant(
                         from: "sogs"
                     )
                     
@@ -45,7 +45,7 @@ class CapabilitiesSpec: QuickSpec {
                 }
                 
                 it("wraps an unknown value in the unsupported case") {
-                    let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability(
+                    let capability: Capability.Variant = Capability.Variant(
                         from: "test"
                     )
                     
@@ -55,12 +55,12 @@ class CapabilitiesSpec: QuickSpec {
             
             context("when accessing the rawValue") {
                 it("provides known cases exactly") {
-                    expect(OpenGroupAPI.Capabilities.Capability.sogs.rawValue).to(equal("sogs"))
-                    expect(OpenGroupAPI.Capabilities.Capability.blind.rawValue).to(equal("blind"))
+                    expect(Capability.Variant.sogs.rawValue).to(equal("sogs"))
+                    expect(Capability.Variant.blind.rawValue).to(equal("blind"))
                 }
                 
                 it("provides the wrapped value for unsupported cases") {
-                    expect(OpenGroupAPI.Capabilities.Capability.unsupported("test").rawValue).to(equal("test"))
+                    expect(Capability.Variant.unsupported("test").rawValue).to(equal("test"))
                 }
             }
             
@@ -68,14 +68,14 @@ class CapabilitiesSpec: QuickSpec {
                 it("decodes known cases exactly") {
                     expect(
                         try? JSONDecoder().decode(
-                            OpenGroupAPI.Capabilities.Capability.self,
+                            Capability.Variant.self,
                             from: "\"sogs\"".data(using: .utf8)!
                         )
                     )
                     .to(equal(.sogs))
                     expect(
                         try? JSONDecoder().decode(
-                            OpenGroupAPI.Capabilities.Capability.self,
+                            Capability.Variant.self,
                             from: "\"blind\"".data(using: .utf8)!
                         )
                     )
@@ -85,7 +85,7 @@ class CapabilitiesSpec: QuickSpec {
                 it("decodes unknown cases into the unsupported case") {
                     expect(
                         try? JSONDecoder().decode(
-                            OpenGroupAPI.Capabilities.Capability.self,
+                            Capability.Variant.self,
                             from: "\"test\"".data(using: .utf8)!
                         )
                     )
diff --git a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift
index 966a270c4..2b8e7c858 100644
--- a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift	
@@ -76,7 +76,7 @@ class OpenGroupSpec: QuickSpec {
                     )
                     
                     expect(openGroup.debugDescription)
-                        .to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0)"))
+                        .to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0, pollFailureCount: 0)"))
                 }
             }
         }
diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift
index a67c9c3ee..e4edc8803 100644
--- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift	
@@ -1237,6 +1237,7 @@ class OpenGroupAPISpec: QuickSpec {
                             sender: "testSender",
                             posted: 321,
                             edited: nil,
+                            deleted: nil,
                             seqNo: 10,
                             whisper: false,
                             whisperMods: false,
@@ -1605,6 +1606,7 @@ class OpenGroupAPISpec: QuickSpec {
                             sender: "testSender",
                             posted: 321,
                             edited: nil,
+                            deleted: nil,
                             seqNo: 10,
                             whisper: false,
                             whisperMods: false,
diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift
index e14a1099f..9a598a15b 100644
--- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift	
@@ -186,6 +186,7 @@ class OpenGroupManagerSpec: QuickSpec {
                     sender: "05\(TestConstants.publicKey)",
                     posted: 123,
                     edited: nil,
+                    deleted: nil,
                     seqNo: 124,
                     whisper: false,
                     whisperMods: false,
@@ -1277,7 +1278,12 @@ class OpenGroupManagerSpec: QuickSpec {
                             defaultWrite: nil,
                             upload: false,
                             defaultUpload: nil,
-                            details: TestCapabilitiesAndRoomApi.roomData.with(moderators: ["TestMod"], admins: [])
+                            details: TestCapabilitiesAndRoomApi.roomData.with(
+                                moderators: ["TestMod"],
+                                hiddenModerators: [],
+                                admins: [],
+                                hiddenAdmins: []
+                            )
                         )
                         
                         mockStorage.write { db in
@@ -1308,7 +1314,67 @@ class OpenGroupManagerSpec: QuickSpec {
                                     server: "testServer"
                                 ),
                                 profileId: "TestMod",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
+                            )
+                        ))
+                    }
+                    
+                    it("updates for hidden moderators") {
+                        var didComplete: Bool = false   // Prevent multi-threading test bugs
+                        
+                        testPollInfo = OpenGroupAPI.RoomPollInfo(
+                            token: "testRoom",
+                            activeUsers: 10,
+                            admin: false,
+                            globalAdmin: false,
+                            moderator: false,
+                            globalModerator: false,
+                            read: false,
+                            defaultRead: nil,
+                            defaultAccessible: nil,
+                            write: false,
+                            defaultWrite: nil,
+                            upload: false,
+                            defaultUpload: nil,
+                            details: TestCapabilitiesAndRoomApi.roomData.with(
+                                moderators: [],
+                                hiddenModerators: ["TestMod2"],
+                                admins: [],
+                                hiddenAdmins: []
+                            )
+                        )
+                        
+                        mockStorage.write { db in
+                            try OpenGroupManager.handlePollInfo(
+                                db,
+                                pollInfo: testPollInfo,
+                                publicKey: TestConstants.publicKey,
+                                for: "testRoom",
+                                on: "testServer",
+                                dependencies: dependencies
+                            ) { didComplete = true }
+                        }
+                        
+                        expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50))
+                        expect(
+                            mockStorage.read { db in
+                                try GroupMember
+                                    .filter(GroupMember.Columns.groupId == OpenGroup.idFor(
+                                        roomToken: "testRoom",
+                                        server: "testServer"
+                                    ))
+                                    .fetchOne(db)
+                            }
+                        ).to(equal(
+                            GroupMember(
+                                groupId: OpenGroup.idFor(
+                                    roomToken: "testRoom",
+                                    server: "testServer"
+                                ),
+                                profileId: "TestMod2",
+                                role: .moderator,
+                                isHidden: true
                             )
                         ))
                     }
@@ -1368,7 +1434,12 @@ class OpenGroupManagerSpec: QuickSpec {
                             defaultWrite: nil,
                             upload: false,
                             defaultUpload: nil,
-                            details: TestCapabilitiesAndRoomApi.roomData.with(moderators: [], admins: ["TestAdmin"])
+                            details: TestCapabilitiesAndRoomApi.roomData.with(
+                                moderators: [],
+                                hiddenModerators: [],
+                                admins: ["TestAdmin"],
+                                hiddenAdmins: []
+                            )
                         )
                         
                         mockStorage.write { db in
@@ -1399,7 +1470,67 @@ class OpenGroupManagerSpec: QuickSpec {
                                     server: "testServer"
                                 ),
                                 profileId: "TestAdmin",
-                                role: .admin
+                                role: .admin,
+                                isHidden: false
+                            )
+                        ))
+                    }
+                    
+                    it("updates for hidden admins") {
+                        var didComplete: Bool = false   // Prevent multi-threading test bugs
+                        
+                        testPollInfo = OpenGroupAPI.RoomPollInfo(
+                            token: "testRoom",
+                            activeUsers: 10,
+                            admin: false,
+                            globalAdmin: false,
+                            moderator: false,
+                            globalModerator: false,
+                            read: false,
+                            defaultRead: nil,
+                            defaultAccessible: nil,
+                            write: false,
+                            defaultWrite: nil,
+                            upload: false,
+                            defaultUpload: nil,
+                            details: TestCapabilitiesAndRoomApi.roomData.with(
+                                moderators: [],
+                                hiddenModerators: [],
+                                admins: [],
+                                hiddenAdmins: ["TestAdmin2"]
+                            )
+                        )
+                        
+                        mockStorage.write { db in
+                            try OpenGroupManager.handlePollInfo(
+                                db,
+                                pollInfo: testPollInfo,
+                                publicKey: TestConstants.publicKey,
+                                for: "testRoom",
+                                on: "testServer",
+                                dependencies: dependencies
+                            ) { didComplete = true }
+                        }
+                        
+                        expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50))
+                        expect(
+                            mockStorage.read { db in
+                                try GroupMember
+                                    .filter(GroupMember.Columns.groupId == OpenGroup.idFor(
+                                        roomToken: "testRoom",
+                                        server: "testServer"
+                                    ))
+                                    .fetchOne(db)
+                            }
+                        ).to(equal(
+                            GroupMember(
+                                groupId: OpenGroup.idFor(
+                                    roomToken: "testRoom",
+                                    server: "testServer"
+                                ),
+                                profileId: "TestAdmin2",
+                                role: .admin,
+                                isHidden: true
                             )
                         ))
                     }
@@ -1978,6 +2109,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                     sender: nil,
                                     posted: 123,
                                     edited: nil,
+                                    deleted: nil,
                                     seqNo: 124,
                                     whisper: false,
                                     whisperMods: false,
@@ -2039,6 +2171,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                     sender: nil,
                                     posted: 123,
                                     edited: nil,
+                                    deleted: nil,
                                     seqNo: 124,
                                     whisper: false,
                                     whisperMods: false,
@@ -2071,6 +2204,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                     sender: "05\(TestConstants.publicKey)",
                                     posted: 123,
                                     edited: nil,
+                                    deleted: nil,
                                     seqNo: 124,
                                     whisper: false,
                                     whisperMods: false,
@@ -2114,6 +2248,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                     sender: "05\(TestConstants.publicKey)",
                                     posted: 122,
                                     edited: nil,
+                                    deleted: nil,
                                     seqNo: 123,
                                     whisper: false,
                                     whisperMods: false,
@@ -2152,6 +2287,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                         sender: "05\(TestConstants.publicKey)",
                                         posted: 123,
                                         edited: nil,
+                                        deleted: nil,
                                         seqNo: 123,
                                         whisper: false,
                                         whisperMods: false,
@@ -2180,6 +2316,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                         sender: "05\(TestConstants.publicKey)",
                                         posted: 123,
                                         edited: nil,
+                                        deleted: nil,
                                         seqNo: 123,
                                         whisper: false,
                                         whisperMods: false,
@@ -2328,6 +2465,10 @@ class OpenGroupManagerSpec: QuickSpec {
                         mockSodium
                             .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
                             .thenReturn(Data(hex: testDirectMessage.sender.removingIdPrefixIfNeeded()).bytes)
+                        
+                        mockSodium
+                            .when { $0.sessionId(any(), matchesBlindedId: any(), serverPublicKey: any(), genericHash: mockGenericHash) }
+                            .thenReturn(false)
                     }
                     
                     it("updates the inbox latest message id") {
@@ -2422,6 +2563,10 @@ class OpenGroupManagerSpec: QuickSpec {
                         mockSodium
                             .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
                             .thenReturn(Data(hex: testDirectMessage.recipient.removingIdPrefixIfNeeded()).bytes)
+                        
+                        mockSodium
+                            .when { $0.sessionId(any(), matchesBlindedId: any(), serverPublicKey: any(), genericHash: mockGenericHash) }
+                            .thenReturn(false)
                     }
                     
                     it("updates the outbox latest message id") {
@@ -2602,7 +2747,8 @@ class OpenGroupManagerSpec: QuickSpec {
                         try GroupMember(
                             groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                             profileId: "05\(TestConstants.publicKey)",
-                            role: .moderator
+                            role: .moderator,
+                            isHidden: false
                         ).insert(db)
                     }
                     
@@ -2621,7 +2767,48 @@ class OpenGroupManagerSpec: QuickSpec {
                         try GroupMember(
                             groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                             profileId: "05\(TestConstants.publicKey)",
-                            role: .admin
+                            role: .admin,
+                            isHidden: false
+                        ).insert(db)
+                    }
+                    
+                    expect(
+                        OpenGroupManager.isUserModeratorOrAdmin(
+                            "05\(TestConstants.publicKey)",
+                            for: "testRoom",
+                            on: "testServer",
+                            using: dependencies
+                        )
+                    ).to(beTrue())
+                }
+                
+                it("returns true if the moderator is hidden") {
+                    mockStorage.write { db in
+                        try GroupMember(
+                            groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
+                            profileId: "05\(TestConstants.publicKey)",
+                            role: .moderator,
+                            isHidden: true
+                        ).insert(db)
+                    }
+                    
+                    expect(
+                        OpenGroupManager.isUserModeratorOrAdmin(
+                            "05\(TestConstants.publicKey)",
+                            for: "testRoom",
+                            on: "testServer",
+                            using: dependencies
+                        )
+                    ).to(beTrue())
+                }
+                
+                it("returns true if the admin is hidden") {
+                    mockStorage.write { db in
+                        try GroupMember(
+                            groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
+                            profileId: "05\(TestConstants.publicKey)",
+                            role: .admin,
+                            isHidden: true
                         ).insert(db)
                     }
                     
@@ -2672,7 +2859,8 @@ class OpenGroupManagerSpec: QuickSpec {
                             try GroupMember(
                                 groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                                 profileId: "00\(otherKey)",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
                             ).insert(db)
                             
                             try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
@@ -2709,7 +2897,8 @@ class OpenGroupManagerSpec: QuickSpec {
                             try GroupMember(
                                 groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                                 profileId: "15\(otherKey)",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
                             ).insert(db)
                         }
                         
@@ -2766,7 +2955,8 @@ class OpenGroupManagerSpec: QuickSpec {
                             try GroupMember(
                                 groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                                 profileId: "05\(otherKey)",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
                             ).insert(db)
                             
                             try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
@@ -2805,7 +2995,8 @@ class OpenGroupManagerSpec: QuickSpec {
                             try GroupMember(
                                 groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                                 profileId: "15\(otherKey)",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
                             ).insert(db)
                             
                             try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
@@ -2911,7 +3102,8 @@ class OpenGroupManagerSpec: QuickSpec {
                             try GroupMember(
                                 groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                                 profileId: "05\(otherKey)",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
                             ).insert(db)
                             
                             try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db)
@@ -2951,7 +3143,8 @@ class OpenGroupManagerSpec: QuickSpec {
                             try GroupMember(
                                 groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
                                 profileId: "00\(otherKey)",
-                                role: .moderator
+                                role: .moderator,
+                                isHidden: false
                             ).insert(db)
                             
                             try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db)
@@ -2977,6 +3170,7 @@ class OpenGroupManagerSpec: QuickSpec {
             context("when getting the default rooms if needed") {
                 beforeEach {
                     class TestRoomsApi: TestOnionRequestAPI {
+                        static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil)
                         static let roomsData: [OpenGroupAPI.Room] = [
                             TestCapabilitiesAndRoomApi.roomData,
                             OpenGroupAPI.Room(
@@ -3009,7 +3203,26 @@ class OpenGroupManagerSpec: QuickSpec {
                         ]
                         
                         override class var mockResponse: Data? {
-                            return try! JSONEncoder().encode(roomsData)
+                            let responses: [Data] = [
+                                try! JSONEncoder().encode(
+                                    OpenGroupAPI.BatchSubResponse(
+                                        code: 200,
+                                        headers: [:],
+                                        body: capabilitiesData,
+                                        failedToParseBody: false
+                                    )
+                                ),
+                                try! JSONEncoder().encode(
+                                    OpenGroupAPI.BatchSubResponse(
+                                        code: 200,
+                                        headers: [:],
+                                        body: roomsData,
+                                        failedToParseBody: false
+                                    )
+                                )
+                            ]
+                            
+                            return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8)
                         }
                     }
                     dependencies = dependencies.with(onionApi: TestRoomsApi.self)
@@ -3178,6 +3391,7 @@ class OpenGroupManagerSpec: QuickSpec {
                 
                 it("fetches the image for any rooms with images") {
                     class TestRoomsApi: TestOnionRequestAPI {
+                        static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil)
                         static let roomsData: [OpenGroupAPI.Room] = [
                             OpenGroupAPI.Room(
                                 token: "test2",
@@ -3209,7 +3423,26 @@ class OpenGroupManagerSpec: QuickSpec {
                         ]
                         
                         override class var mockResponse: Data? {
-                            return try! JSONEncoder().encode(roomsData)
+                            let responses: [Data] = [
+                                try! JSONEncoder().encode(
+                                    OpenGroupAPI.BatchSubResponse(
+                                        code: 200,
+                                        headers: [:],
+                                        body: capabilitiesData,
+                                        failedToParseBody: false
+                                    )
+                                ),
+                                try! JSONEncoder().encode(
+                                    OpenGroupAPI.BatchSubResponse(
+                                        code: 200,
+                                        headers: [:],
+                                        body: roomsData,
+                                        failedToParseBody: false
+                                    )
+                                )
+                            ]
+                            
+                            return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8)
                         }
                     }
                     let testDate: Date = Date(timeIntervalSince1970: 1234567890)
@@ -3218,7 +3451,9 @@ class OpenGroupManagerSpec: QuickSpec {
                         date: testDate
                     )
                     
-                    OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies)
+                    OpenGroupManager
+                        .getDefaultRoomsIfNeeded(using: dependencies)
+                        .retainUntilComplete()
 
                     expect(mockUserDefaults)
                         .toEventually(
@@ -3674,7 +3909,12 @@ class OpenGroupManagerSpec: QuickSpec {
 // MARK: - Room Convenience Extensions
 
 extension OpenGroupAPI.Room {
-    func with(moderators: [String], admins: [String]) -> OpenGroupAPI.Room {
+    func with(
+        moderators: [String],
+        hiddenModerators: [String],
+        admins: [String],
+        hiddenAdmins: [String]
+    ) -> OpenGroupAPI.Room {
         return OpenGroupAPI.Room(
             token: self.token,
             name: self.name,
@@ -3689,11 +3929,11 @@ extension OpenGroupAPI.Room {
             admin: self.admin,
             globalAdmin: self.globalAdmin,
             admins: admins,
-            hiddenAdmins: self.hiddenAdmins,
+            hiddenAdmins: hiddenAdmins,
             moderator: self.moderator,
             globalModerator: self.globalModerator,
             moderators: moderators,
-            hiddenModerators: self.hiddenModerators,
+            hiddenModerators: hiddenModerators,
             read: self.read,
             defaultRead: self.defaultRead,
             defaultAccessible: self.defaultAccessible,
diff --git a/SessionUtilitiesKit/Database/Models/Identity.swift b/SessionUtilitiesKit/Database/Models/Identity.swift
index cc9749ce8..9d08731f5 100644
--- a/SessionUtilitiesKit/Database/Models/Identity.swift
+++ b/SessionUtilitiesKit/Database/Models/Identity.swift
@@ -129,14 +129,16 @@ public extension Identity {
         )
     }
     
-    static func fetchHexEncodedSeed() -> String? {
-        return Storage.shared.read { db in
-            guard let data: Data = try? Identity.fetchOne(db, id: .seed)?.data else {
-                return nil
-            }
-            
-            return data.toHexString()
+    static func fetchHexEncodedSeed(_ db: Database? = nil) -> String? {
+        guard let db: Database = db else {
+            return Storage.shared.read { db in fetchHexEncodedSeed(db) }
+        }
+        
+        guard let data: Data = try? Identity.fetchOne(db, id: .seed)?.data else {
+            return nil
         }
+        
+        return data.toHexString()
     }
 }
 
diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift
new file mode 100644
index 000000000..4c21fb753
--- /dev/null
+++ b/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift
@@ -0,0 +1,26 @@
+// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
+
+import Foundation
+import GRDB
+
+/// This is a convenience wrapper around the GRDB `TableAlteration` class which allows for shorthand
+/// when creating tables
+public class TypedTableAlteration<T> where T: TableRecord, T: ColumnExpressible {
+    let alteration: TableAlteration
+    
+    init(alteration: TableAlteration) {
+        self.alteration = alteration
+    }
+    
+    @discardableResult public func add(_ key: T.Columns, _ type: Database.ColumnType? = nil) -> ColumnDefinition {
+        return alteration.add(column: key.name, type)
+    }
+    
+    public func rename(column: String, to key: T.Columns) {
+        alteration.rename(column: column, to: key.name)
+    }
+    
+    public func drop(_ key: T.Columns) {
+        return alteration.drop(column: key.name)
+    }
+}
diff --git a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift
index 09a6cb7a5..278f52766 100644
--- a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift
+++ b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift
@@ -16,6 +16,17 @@ public extension Database {
         }
     }
     
+    func alter<T>(
+        table: T.Type,
+        body: (TypedTableAlteration<T>) -> Void
+    ) throws where T: TableRecord, T: ColumnExpressible {
+        try alter(table: T.databaseTableName) { tableAlteration in
+            let typedAlteration: TypedTableAlteration<T> = TypedTableAlteration(alteration: tableAlteration)
+            
+            body(typedAlteration)
+        }
+    }
+    
     func makeFTS5Pattern<T>(rawPattern: String, forTable table: T.Type) throws -> FTS5Pattern where T: TableRecord, T: ColumnExpressible {
         return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName)
     }
diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift
new file mode 100644
index 000000000..135e80dc9
--- /dev/null
+++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift
@@ -0,0 +1,105 @@
+// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
+
+import Foundation
+import GRDB
+
+import Quick
+import Nimble
+
+@testable import SessionUtilitiesKit
+
+class IdentitySpec: QuickSpec {
+    // MARK: - Spec
+
+    override func spec() {
+        var mockStorage: Storage!
+        
+        describe("an Identity") {
+            beforeEach {
+                mockStorage = Storage(
+                    customWriter: DatabaseQueue(),
+                    customMigrations: [
+                        SNUtilitiesKit.migrations()
+                    ]
+                )
+            }
+            
+            it("correctly retrieves the user user public key") {
+                mockStorage.write { db in
+                    try Identity(variant: .x25519PublicKey, data: "Test1".data(using: .utf8)!).insert(db)
+                }
+                
+                mockStorage.read { db in
+                    expect(Identity.fetchUserPublicKey(db))
+                        .to(equal("Test1".data(using: .utf8)))
+                }
+            }
+            
+            it("correctly retrieves the user private key") {
+                mockStorage.write { db in
+                    try Identity(variant: .x25519PrivateKey, data: "Test2".data(using: .utf8)!).insert(db)
+                }
+                
+                mockStorage.read { db in
+                    expect(Identity.fetchUserPrivateKey(db))
+                        .to(equal("Test2".data(using: .utf8)))
+                }
+            }
+            
+            it("correctly retrieves the user key pair") {
+                mockStorage.write { db in
+                    try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db)
+                    try Identity(variant: .x25519PrivateKey, data: "Test4".data(using: .utf8)!).insert(db)
+                }
+                
+                mockStorage.read { db in
+                    let keyPair = Identity.fetchUserKeyPair(db)
+                    
+                    expect(keyPair?.publicKey)
+                        .to(equal("Test3".data(using: .utf8)?.bytes))
+                    expect(keyPair?.secretKey)
+                        .to(equal("Test4".data(using: .utf8)?.bytes))
+                }
+            }
+            
+            it("correctly determines if the user exists") {
+                mockStorage.write { db in
+                    try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db)
+                    try Identity(variant: .x25519PrivateKey, data: "Test4".data(using: .utf8)!).insert(db)
+                }
+                
+                mockStorage.read { db in
+                    expect(Identity.userExists(db))
+                        .to(equal(true))
+                }
+            }
+            
+            it("correctly retrieves the user ED25519 key pair") {
+                mockStorage.write { db in
+                    try Identity(variant: .ed25519PublicKey, data: "Test5".data(using: .utf8)!).insert(db)
+                    try Identity(variant: .ed25519SecretKey, data: "Test6".data(using: .utf8)!).insert(db)
+                }
+                
+                mockStorage.read { db in
+                    let keyPair = Identity.fetchUserEd25519KeyPair(db)
+                    
+                    expect(keyPair?.publicKey)
+                        .to(equal("Test5".data(using: .utf8)?.bytes))
+                    expect(keyPair?.secretKey)
+                        .to(equal("Test6".data(using: .utf8)?.bytes))
+                }
+            }
+            
+            it("correctly retrieves the hex encoded seed") {
+                mockStorage.write { db in
+                    try Identity(variant: .seed, data: "Test7".data(using: .utf8)!).insert(db)
+                }
+                
+                mockStorage.read { db in
+                    expect(Identity.fetchHexEncodedSeed(db))
+                        .to(equal("5465737437"))
+                }
+            }
+        }
+    }
+}