feat: view invite page

This commit is contained in:
Zomatree
2024-09-25 18:23:43 +01:00
parent 6006d425e3
commit 408d8afc8d
11 changed files with 251 additions and 35 deletions
+5
View File
@@ -41,6 +41,7 @@
1759C39C2B291E2F006E6BBE /* SystemMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1759C39B2B291E2F006E6BBE /* SystemMessageView.swift */; };
175E01DE2ADAB3F6004F6431 /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175E01DD2ADAB3F6004F6431 /* LazyImage.swift */; };
175E01E02ADAB554004F6431 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175E01DF2ADAB554004F6431 /* Avatar.swift */; };
176FA6E82C9336A900EF73C0 /* ViewInvite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176FA6E72C9336A900EF73C0 /* ViewInvite.swift */; };
1773C0272C07DC32007B8867 /* Types.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17DE7DB22BFAEC3F00C99188 /* Types.framework */; platformFilters = (ios, maccatalyst, macos, ); };
1773C0282C07DC32007B8867 /* Types.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 17DE7DB22BFAEC3F00C99188 /* Types.framework */; platformFilters = (ios, maccatalyst, macos, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1773C02F2C07DC99007B8867 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DE7E0C2BFAFDDD00C99188 /* Api.swift */; };
@@ -88,6 +89,7 @@
17BF54D12B17B24200178866 /* LanguageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BF54D02B17B24200178866 /* LanguageSettings.swift */; };
17C444582B06CC5400F97E4A /* ForgotPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C444572B06CC5400F97E4A /* ForgotPassword.swift */; };
17C9A3502B0580D10043A387 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C9A34F2B0580D10043A387 /* Welcome.swift */; };
17CC0CB42C937D4F005BA3A6 /* ServerBadges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CC0CB32C937D4F005BA3A6 /* ServerBadges.swift */; };
17CCCF6E2ADA173B00D78D7A /* ULID in Frameworks */ = {isa = PBXBuildFile; productRef = 17CCCF6D2ADA173B00D78D7A /* ULID */; };
17CCCF702ADA17B600D78D7A /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CCCF6F2ADA17B600D78D7A /* Utils.swift */; };
17CE2E142AE709CE0059FB3A /* UserSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CE2E132AE709CE0059FB3A /* UserSheet.swift */; };
@@ -216,6 +218,7 @@
1759C39B2B291E2F006E6BBE /* SystemMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMessageView.swift; sourceTree = "<group>"; };
175E01DD2ADAB3F6004F6431 /* LazyImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyImage.swift; sourceTree = "<group>"; };
175E01DF2ADAB554004F6431 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
176FA6E72C9336A900EF73C0 /* ViewInvite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewInvite.swift; sourceTree = "<group>"; };
1773C03C2C07DD1F007B8867 /* MessageableChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageableChannel.swift; sourceTree = "<group>"; };
17772C172C30AF83000D1EDA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
1777DD832ADC24CE003D6C72 /* ReversedSrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversedSrollView.swift; sourceTree = "<group>"; };
@@ -415,6 +418,7 @@
17BF54CC2B1785E200178866 /* HomeWelcome.swift */,
17B7128C2B03E9D700CFF61C /* FriendsList.swift */,
17F502532B9BFB1000A3022D /* AddFriend.swift */,
176FA6E72C9336A900EF73C0 /* ViewInvite.swift */,
);
path = Home;
sourceTree = "<group>";
@@ -910,6 +914,7 @@
170C23D52C224B6D0057E399 /* SettingsCommon.swift in Sources */,
170C23D62C224B6D0057E399 /* UserSettings.swift in Sources */,
178BB11B2B02FCDD001143A4 /* Responses.swift in Sources */,
176FA6E82C9336A900EF73C0 /* ViewInvite.swift in Sources */,
173D698E2C7FF7FC00030E62 /* ChannelSearch.swift in Sources */,
17A589FA2C44781F00B9D85A /* AnyTransition.swift in Sources */,
17BF54CD2B1785E200178866 /* HomeWelcome.swift in Sources */,
+4
View File
@@ -363,4 +363,8 @@ struct HTTPClient {
func fetchMutuals(user: String) async -> Result<MutualsResponse, RevoltError> {
await req(method: .get, route: "/users/\(user)/mutual")
}
func fetchInvite(code: String) async -> Result<InviteInfoResponse, RevoltError> {
await req(method: .get, route: "/invites/\(code)")
}
}
+47
View File
@@ -84,3 +84,50 @@ struct MutualsResponse: Decodable {
var servers: [String]
var users: [String]
}
enum InviteInfoResponse {
case server(ServerInfoResponse)
case group(GroupInfoResponse)
}
extension InviteInfoResponse: Decodable {
enum CodingKeys: String, CodingKey { case type }
enum Tag: String, Codable { case Server, Group }
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let singleValueContainer = try decoder.singleValueContainer()
switch try container.decode(Tag.self, forKey: .type) {
case .Server:
self = .server(try singleValueContainer.decode(ServerInfoResponse.self))
case .Group:
self = .group(try singleValueContainer.decode(GroupInfoResponse.self))
}
}
}
struct ServerInfoResponse: Decodable {
var code: String
var server_id: String
var server_name: String
var server_icon: File?
var server_banner: File?
var server_flags: ServerFlags?
var channel_id: String
var channel_name: String
var channel_description: String?
var user_name: String
var user_avatar: File?
var member_count: Int
}
struct GroupInfoResponse: Decodable {
var code: String
var channel_id: String
var channel_name: String
var channel_description: String?
var user_name: String
var user_avatar: File?
}
@@ -154,24 +154,7 @@ struct ServerChannelScrollView: View {
}
HStack(alignment: .center, spacing: 8) {
if server.flags?.contains(.offical) == true {
ZStack(alignment: .center) {
Image(systemName: "seal.fill")
.resizable()
.frame(width: 12, height: 12)
.foregroundStyle(.white)
Image("monochrome")
.resizable()
.frame(width: 10, height: 10)
.colorInvert()
}
} else if server.flags?.contains(.verified) == true {
Image(systemName: "checkmark.seal.fill")
.resizable()
.foregroundStyle(.black, .white)
.frame(width: 12, height: 12)
}
ServerBadges(value: server.flags)
Text(server.name)
.fontWeight(.medium)
+35
View File
@@ -0,0 +1,35 @@
//
// ServerBadges.swift
// Revolt
//
// Created by Angelo on 12/09/2024.
//
import Foundation
import SwiftUI
import Types
struct ServerBadges: View {
var value: ServerFlags?
var body: some View {
if value?.contains(.offical) == true {
ZStack(alignment: .center) {
Image(systemName: "seal.fill")
.resizable()
.frame(width: 12, height: 12)
.foregroundStyle(.white)
Image("monochrome")
.resizable()
.frame(width: 10, height: 10)
.colorInvert()
}
} else if value?.contains(.verified) == true {
Image(systemName: "checkmark.seal.fill")
.resizable()
.foregroundStyle(.black, .white)
.frame(width: 12, height: 12)
}
}
}
+22 -10
View File
@@ -22,20 +22,32 @@ struct ServerIcon<S: Shape>: View {
if let icon = server.icon {
LazyImage(source: .file(icon), height: height, width: height, clipTo: clipTo)
} else {
ZStack(alignment: .center) {
let firstChar = server.name.first ?? "?"
clipTo
.fill(viewState.theme.background3) // TODO: background3
.frame(width: width, height: height)
Text(verbatim: "\(firstChar)")
.bold()
}
FallbackServerIcon(name: server.name, width: width, height: height, clipTo: clipTo)
}
}
}
struct FallbackServerIcon<S: Shape>: View {
@EnvironmentObject var viewState: ViewState
var name: String
var width: CGFloat?
var height: CGFloat?
var clipTo: S
var body: some View {
ZStack(alignment: .center) {
let firstChar = name.first ?? "?"
clipTo
.fill(viewState.theme.background3)
.frame(width: width, height: height)
Text(verbatim: "\(firstChar)")
.bold()
}
}
}
struct ServerListIcon: View {
@EnvironmentObject var viewState: ViewState
+12 -3
View File
@@ -70,14 +70,23 @@ struct JoinServerAlert: View {
@State var text: String = ""
func parseInvite() -> String? {
if let match = text.wholeMatch(of: /(?:(?:https?:\/\/)?rvlt\.gg\/)?(\w+)/) {
return String(match.output.1)
} else {
return nil
}
}
var body: some View {
TextField("Invite code or link", text: $text)
Button("Join") {
Task {
let join = try! await viewState.http.joinServer(code: text).get()
viewState.servers[join.server.id] = join.server
viewState.selectServer(withId: join.server.id)
if let invite_code = parseInvite(), (try! await viewState.http.fetchInvite(code: invite_code).get()) != nil {
viewState.path.append(NavigationDestination.invite(invite_code))
}
}
}
@@ -29,10 +29,15 @@ struct ServerInfoSheet: View {
ServerIcon(server: server, height: 64, width: 64, clipTo: Rectangle())
.addBorder(.black, width: 2, cornerRadius: 8)
Text(server.name)
.bold()
.padding(.horizontal, 8)
.padding(.bottom, 8)
HStack(alignment: .center, spacing: 8) {
ServerBadges(value: server.flags)
Text(server.name)
.bold()
}
.padding(.horizontal, 8)
.padding(.bottom, 8)
}
.padding(.top, server.banner == nil ? 8 : 0)
.padding(.horizontal, 8)
+113
View File
@@ -0,0 +1,113 @@
//
// ViewInvite.swift
// Revolt
//
// Created by Angelo on 12/09/2024.
//
import Foundation
import SwiftUI
struct ViewInvite: View {
@EnvironmentObject var viewState: ViewState
var code: String
@State var info: InviteInfoResponse?? = nil
var body: some View {
ZStack {
switch info {
case .none:
LoadingSpinnerView(frameSize: CGSize(width: 32, height: 32), isActionComplete: .constant(false))
case .some(.none):
Text("Invalid invite")
case .group(let groupInfo):
Text("Group TODO")
case .server(let serverInfo):
if let banner = serverInfo.server_banner {
LazyImage(source: .file(banner), clipTo: Rectangle())
}
VStack(spacing: 16) {
Group {
if let server_icon = serverInfo.server_icon {
LazyImage(source: .file(server_icon), clipTo: Circle())
} else {
FallbackServerIcon(name: serverInfo.server_name, clipTo: Circle())
}
}
.frame(width: 48, height: 48)
HStack(alignment: .center, spacing: 8) {
ServerBadges(value: serverInfo.server_flags)
Text(verbatim: serverInfo.server_name)
.fontWeight(.medium)
}
HStack {
Text("#\(serverInfo.channel_name)")
Circle()
.frame(width: 4, height: 4)
Text("\(serverInfo.member_count) users")
}
.foregroundStyle(viewState.theme.foreground2)
HStack {
Text("Invited by")
HStack {
if let avatar = serverInfo.user_avatar {
LazyImage(source: .file(avatar), clipTo: Circle())
.frame(width: 24, height: 24)
}
Text(verbatim: serverInfo.user_name)
}
}
Button {
Task {
let join = try! await viewState.http.joinServer(code: code).get()
viewState.servers[join.server.id] = join.server
for channel in join.channels {
viewState.channels[channel.id] = channel
viewState.channelMessages[channel.id] = []
}
viewState.selectChannel(inServer: serverInfo.server_id, withId: serverInfo.channel_id)
}
} label: {
Text("Accept Invite")
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
.background(viewState.theme.background2)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.padding(16)
.background(viewState.theme.background.opacity(0.95))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.toolbarBackground(viewState.theme.topBar, for: .automatic)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
Text("Join Invite")
}
}
}
.task {
if let info = try? await viewState.http.fetchInvite(code: code).get() {
self.info = info
} else {
self.info = .some(.none)
}
}
}
}
+2
View File
@@ -226,6 +226,8 @@ struct InnerApp: View {
case .channel_search(let id):
let channel = Binding($viewState.channels[id])!
ChannelSearch(channel: channel)
case .invite(let code):
ViewInvite(code: code)
}
}
.sheet(item: $viewState.currentUserSheet) { (v) in
+1
View File
@@ -116,6 +116,7 @@ enum NavigationDestination: Hashable, Codable {
case create_group([String])
case create_server
case channel_search(String)
case invite(String)
}
struct UserMaybeMember: Identifiable {