mirror of
https://github.com/stoatchat/for-ios.git
synced 2026-07-18 16:04:32 -04:00
feat: view invite page
This commit is contained in:
@@ -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 */,
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user