mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
ui background matches theme
ui bg is 70% opacity
This commit is contained in:
@@ -14,30 +14,35 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
ZStack {
|
||||||
SessionsListView(
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
handler: handler,
|
.ignoresSafeArea(.all)
|
||||||
hostsManager: hostsManager,
|
List {
|
||||||
keyManager: keyManager
|
SessionsListView(
|
||||||
)
|
handler: handler,
|
||||||
|
hostsManager: hostsManager,
|
||||||
HostsView(
|
keyManager: keyManager
|
||||||
handler: handler,
|
)
|
||||||
hostsManager: hostsManager,
|
|
||||||
keyManager: keyManager
|
HostsView(
|
||||||
)
|
handler: handler,
|
||||||
|
hostsManager: hostsManager,
|
||||||
NavigationLink {
|
keyManager: keyManager
|
||||||
KeyManagerView(hostsManager: hostsManager, keyManager: keyManager)
|
)
|
||||||
} label: {
|
|
||||||
Label("Keys", systemImage: "key.fill")
|
NavigationLink {
|
||||||
}
|
KeyManagerView(hostsManager: hostsManager, keyManager: keyManager)
|
||||||
|
} label: {
|
||||||
NavigationLink {
|
Label("Keys", systemImage: "key.fill")
|
||||||
HostkeysView(hostsManager: hostsManager)
|
}
|
||||||
} label: {
|
|
||||||
Label("Hostkey Fingerprints", systemImage: "lock.display")
|
NavigationLink {
|
||||||
|
HostkeysView(hostsManager: hostsManager)
|
||||||
|
} label: {
|
||||||
|
Label("Hostkey Fingerprints", systemImage: "lock.display")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ struct ConnectionView: View {
|
|||||||
@State var hostKeyChangedAlert: Bool = false
|
@State var hostKeyChangedAlert: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
ZStack {
|
||||||
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
|
.ignoresSafeArea(.all)
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
@@ -128,6 +130,34 @@ struct ConnectionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.transition(.opacity)
|
||||||
|
.onChange(of: handler.host.key) { _ in
|
||||||
|
guard let previousKnownHost = hostsManager.getHostMatching(handler.host) else { return }
|
||||||
|
guard handler.host.key == previousKnownHost.key else {
|
||||||
|
hostKeyChangedAlert = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
hostsManager.updateHost(handler.host)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
if let publicKeyData = handler.host.publicKey {
|
||||||
|
pubkeyStr = String(data: publicKeyData, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
if let privateKeyData = handler.host.privateKey {
|
||||||
|
privkeyStr = String(data: privateKeyData, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if shellView == nil {
|
||||||
|
shellView = ShellView(handler: handler, hostsManager: hostsManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
hostsManager.addHostIfNeeded(handler.host)
|
||||||
|
}
|
||||||
.alert("Hostkey changed", isPresented: $hostKeyChangedAlert) {
|
.alert("Hostkey changed", isPresented: $hostKeyChangedAlert) {
|
||||||
Button("Accept New Hostkey", role: .destructive) {
|
Button("Accept New Hostkey", role: .destructive) {
|
||||||
hostsManager.updateHost(handler.host)
|
hostsManager.updateHost(handler.host)
|
||||||
@@ -141,7 +171,6 @@ struct ConnectionView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Expected \(handler.host.key ?? "nil")\nbut recieved \(handler.getHostkey() ?? "nil") from the server")
|
Text("Expected \(handler.host.key ?? "nil")\nbut recieved \(handler.getHostkey() ?? "nil") from the server")
|
||||||
}
|
}
|
||||||
.transition(.opacity)
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem() {
|
ToolbarItem() {
|
||||||
Button() {
|
Button() {
|
||||||
@@ -156,40 +185,14 @@ struct ConnectionView: View {
|
|||||||
.disabled(handler.hostInvalid())
|
.disabled(handler.hostInvalid())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.fullScreenCover(isPresented: $showTerminal) {
|
||||||
.fullScreenCover(isPresented: $showTerminal) {
|
if let shellView {
|
||||||
if let shellView {
|
shellView
|
||||||
shellView
|
} else {
|
||||||
} else {
|
Text("no shellview")
|
||||||
Text("no shellview")
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: handler.host.key) { _ in
|
|
||||||
guard let previousKnownHost = hostsManager.getHostMatching(handler.host) else { return }
|
|
||||||
guard handler.host.key == previousKnownHost.key else {
|
|
||||||
hostKeyChangedAlert = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
hostsManager.updateHost(handler.host)
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
if let publicKeyData = handler.host.publicKey {
|
|
||||||
pubkeyStr = String(data: publicKeyData, encoding: .utf8) ?? ""
|
|
||||||
}
|
|
||||||
if let privateKeyData = handler.host.privateKey {
|
|
||||||
privkeyStr = String(data: privateKeyData, encoding: .utf8) ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if shellView == nil {
|
|
||||||
shellView = ShellView(handler: handler, hostsManager: hostsManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
hostsManager.addHostIfNeeded(handler.host)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,47 +10,52 @@ import SwiftUI
|
|||||||
struct HostkeysView: View {
|
struct HostkeysView: View {
|
||||||
@ObservedObject var hostsManager: HostsManager
|
@ObservedObject var hostsManager: HostsManager
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
ZStack {
|
||||||
List {
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
if hostsManager.hosts.isEmpty {
|
.ignoresSafeArea(.all)
|
||||||
VStack(alignment: .leading) {
|
NavigationStack {
|
||||||
Text("Looking empty 'round here...")
|
List {
|
||||||
.font(.title3)
|
if hostsManager.hosts.isEmpty {
|
||||||
.bold()
|
|
||||||
.padding(.bottom)
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Connect to some hosts to collect more hostkeys!")
|
Text("Looking empty 'round here...")
|
||||||
|
.font(.title3)
|
||||||
|
.bold()
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.")
|
VStack(alignment: .leading) {
|
||||||
.font(.subheadline)
|
Text("Connect to some hosts to collect more hostkeys!")
|
||||||
Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.")
|
.padding(.bottom)
|
||||||
.font(.subheadline)
|
Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.")
|
||||||
|
.font(.subheadline)
|
||||||
|
Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.")
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
ForEach(hostsManager.hosts) { host in
|
||||||
ForEach(hostsManager.hosts) { host in
|
VStack(alignment: .leading) {
|
||||||
VStack(alignment: .leading) {
|
if !host.name.isEmpty {
|
||||||
if !host.name.isEmpty {
|
Text("name")
|
||||||
Text("name")
|
.foregroundStyle(.gray)
|
||||||
|
.font(.caption)
|
||||||
|
Text(host.name)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
Text("address")
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
Text(host.name)
|
Text(host.address)
|
||||||
.bold()
|
.bold()
|
||||||
|
Text(host.key ?? "nil")
|
||||||
}
|
}
|
||||||
Text("address")
|
|
||||||
.foregroundStyle(.gray)
|
|
||||||
.font(.caption)
|
|
||||||
Text(host.address)
|
|
||||||
.bold()
|
|
||||||
Text(host.key ?? "nil")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.navigationTitle("Hostkeys")
|
||||||
}
|
}
|
||||||
.navigationTitle("Hostkeys")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@@ -12,63 +12,68 @@ struct KeyDetailView: View {
|
|||||||
@State var keypair: Keypair
|
@State var keypair: Keypair
|
||||||
@State private var reveal: Bool = false
|
@State private var reveal: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
ZStack {
|
||||||
VStack(alignment: .leading) {
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
Text("Used on")
|
.ignoresSafeArea(.all)
|
||||||
.bold()
|
List {
|
||||||
ForEach(hostsManager.getHostsKeysUsedOn([keypair])) { host in
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
Text("Used on")
|
||||||
SymbolPreview(symbol: host.symbol, label: host.label)
|
.bold()
|
||||||
.frame(width: 40, height: 40)
|
ForEach(hostsManager.getHostsKeysUsedOn([keypair])) { host in
|
||||||
Text(hostsManager.makeLabel(forHost: host))
|
HStack {
|
||||||
}
|
SymbolPreview(symbol: host.symbol, label: host.label)
|
||||||
}
|
.frame(width: 40, height: 40)
|
||||||
}
|
Text(hostsManager.makeLabel(forHost: host))
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("Public key")
|
|
||||||
.bold()
|
|
||||||
Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil")
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("Private key")
|
|
||||||
.bold()
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
Text(String(data: keypair.privateKey!, encoding: .utf8) ?? "nil")
|
|
||||||
.blur(radius: reveal ? 0 : 5)
|
|
||||||
VStack {
|
|
||||||
Image(systemName: "eye.slash.fill")
|
|
||||||
.resizable().scaledToFit()
|
|
||||||
.frame(width: 50)
|
|
||||||
Text("Tap to reveal")
|
|
||||||
}
|
|
||||||
.opacity(reveal ? 0 : 1)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.onTapGesture {
|
|
||||||
Task {
|
|
||||||
if !reveal {
|
|
||||||
guard await hostsManager.authWithBiometrics() else { return }
|
|
||||||
}
|
}
|
||||||
withAnimation(.spring) { reveal.toggle() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
VStack(alignment: .leading) {
|
||||||
|
Text("Public key")
|
||||||
Button {
|
.bold()
|
||||||
Task {
|
Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil")
|
||||||
guard await hostsManager.authWithBiometrics() else { return }
|
}
|
||||||
if let privateKey = keypair.privateKey {
|
VStack(alignment: .leading) {
|
||||||
UIPasteboard.general.string = String(data: privateKey, encoding: .utf8)
|
Text("Private key")
|
||||||
|
.bold()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
Text(String(data: keypair.privateKey!, encoding: .utf8) ?? "nil")
|
||||||
|
.blur(radius: reveal ? 0 : 5)
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "eye.slash.fill")
|
||||||
|
.resizable().scaledToFit()
|
||||||
|
.frame(width: 50)
|
||||||
|
Text("Tap to reveal")
|
||||||
|
}
|
||||||
|
.opacity(reveal ? 0 : 1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.onTapGesture {
|
||||||
|
Task {
|
||||||
|
if !reveal {
|
||||||
|
guard await hostsManager.authWithBiometrics() else { return }
|
||||||
|
}
|
||||||
|
withAnimation(.spring) { reveal.toggle() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
|
||||||
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
|
Button {
|
||||||
|
Task {
|
||||||
|
guard await hostsManager.authWithBiometrics() else { return }
|
||||||
|
if let privateKey = keypair.privateKey {
|
||||||
|
UIPasteboard.general.string = String(data: privateKey, encoding: .utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
|
||||||
|
}
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
.listRowSeparator(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@@ -12,32 +12,37 @@ struct KeyManagerView: View {
|
|||||||
@ObservedObject var keyManager: KeyManager
|
@ObservedObject var keyManager: KeyManager
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
ZStack {
|
||||||
List {
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
Section {
|
.ignoresSafeArea(.all)
|
||||||
ForEach(hostsManager.getKeys()) { keypair in
|
NavigationStack {
|
||||||
NavigationLink {
|
List {
|
||||||
KeyDetailView(hostsManager: hostsManager, keypair: keypair)
|
Section {
|
||||||
} label: {
|
ForEach(hostsManager.getKeys()) { keypair in
|
||||||
if let publicKey = keypair.publicKey {
|
NavigationLink {
|
||||||
Text(String(data: publicKey, encoding: .utf8) ?? "nil")
|
KeyDetailView(hostsManager: hostsManager, keypair: keypair)
|
||||||
|
} label: {
|
||||||
|
if let publicKey = keypair.publicKey {
|
||||||
|
Text(String(data: publicKey, encoding: .utf8) ?? "nil")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Button("ed25519") {
|
||||||
Button("ed25519") {
|
keyManager.generateEd25519()
|
||||||
keyManager.generateEd25519()
|
}
|
||||||
}
|
Button("rsa") {
|
||||||
Button("rsa") {
|
do {
|
||||||
do {
|
try keyManager.generateRSA()
|
||||||
try keyManager.generateRSA()
|
} catch {
|
||||||
} catch {
|
print(error.localizedDescription)
|
||||||
print(error.localizedDescription)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.navigationTitle("Keys")
|
||||||
}
|
}
|
||||||
.navigationTitle("Keys")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,75 +25,79 @@ struct ThemeManagerView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geo in
|
ZStack {
|
||||||
let columns: Int = Int(geo.size.width)/200
|
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
|
||||||
let layout = Array(repeating: grid, count: columns)
|
.ignoresSafeArea(.all)
|
||||||
ScrollView {
|
GeometryReader { geo in
|
||||||
if hostsManager.themes.isEmpty {
|
let columns: Int = Int(geo.size.width)/200
|
||||||
VStack(alignment: .leading) {
|
let layout = Array(repeating: grid, count: columns)
|
||||||
Image(systemName: "paintpalette")
|
ScrollView {
|
||||||
.resizable().scaledToFit()
|
if hostsManager.themes.isEmpty {
|
||||||
.symbolRenderingMode(.multicolor)
|
VStack(alignment: .leading) {
|
||||||
.frame(width: 50)
|
Image(systemName: "paintpalette")
|
||||||
Text("No themes (yet)")
|
.resizable().scaledToFit()
|
||||||
.font(.title)
|
.symbolRenderingMode(.multicolor)
|
||||||
.padding(.vertical, 10)
|
.frame(width: 50)
|
||||||
.bold()
|
Text("No themes (yet)")
|
||||||
Text("Tap the Safari icon at the top right to find themes!")
|
.font(.title)
|
||||||
Text("Once you find one that you like, copy it's link and enter it here using the link button.")
|
.padding(.vertical, 10)
|
||||||
|
.bold()
|
||||||
|
Text("Tap the Safari icon at the top right to find themes!")
|
||||||
|
Text("Once you find one that you like, copy it's link and enter it here using the link button.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
|
||||||
|
ForEach(hostsManager.themes) { theme in
|
||||||
|
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.animation(.default, value: hostsManager.themes)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Built-in Themes")
|
||||||
|
.padding(.top)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
|
LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
|
||||||
ForEach(hostsManager.themes) { theme in
|
ForEach(Theme.builtinThemes) { theme in
|
||||||
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: true)
|
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.animation(.default, value: hostsManager.themes)
|
.animation(.default, value: hostsManager.themes)
|
||||||
}
|
}
|
||||||
|
.navigationTitle("Themes")
|
||||||
HStack {
|
.alert("Enter URL", isPresented: $showAlert) {
|
||||||
Text("Built-in Themes")
|
TextField("", text: $importURL, prompt: Text("URL"))
|
||||||
.padding(.top)
|
Button("Cancel") {}
|
||||||
.padding(.horizontal)
|
|
||||||
.font(.headline)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
LazyVGrid(columns: layout, alignment: .center, spacing: 8) {
|
|
||||||
ForEach(Theme.builtinThemes) { theme in
|
|
||||||
ThemePreview(hostsManager: hostsManager, theme: theme, canModify: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.animation(.default, value: hostsManager.themes)
|
|
||||||
}
|
|
||||||
.navigationTitle("Themes")
|
|
||||||
.alert("Enter URL", isPresented: $showAlert) {
|
|
||||||
TextField("", text: $importURL, prompt: Text("URL"))
|
|
||||||
Button("Cancel") {}
|
|
||||||
Button() {
|
|
||||||
hostsManager.downloadTheme(fromUrl: URL(string: importURL))
|
|
||||||
importURL = ""
|
|
||||||
} label: {
|
|
||||||
Label("Import", systemImage: "square.and.arrow.down")
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem() {
|
|
||||||
Button() {
|
Button() {
|
||||||
UIApplication.shared.open(URL(string: "https://iterm2colorschemes.com")!)
|
hostsManager.downloadTheme(fromUrl: URL(string: importURL))
|
||||||
|
importURL = ""
|
||||||
} label: {
|
} label: {
|
||||||
Label("Open themes site", systemImage: "safari")
|
Label("Import", systemImage: "square.and.arrow.down")
|
||||||
|
.bold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolbarItem() {
|
.toolbar {
|
||||||
Button() {
|
ToolbarItem() {
|
||||||
showAlert.toggle()
|
Button() {
|
||||||
} label: {
|
UIApplication.shared.open(URL(string: "https://iterm2colorschemes.com")!)
|
||||||
Label("From URL", systemImage: "link")
|
} label: {
|
||||||
|
Label("Open themes site", systemImage: "safari")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem() {
|
||||||
|
Button() {
|
||||||
|
showAlert.toggle()
|
||||||
|
} label: {
|
||||||
|
Label("From URL", systemImage: "link")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user