mirror of
https://github.com/Mueller-Patrick/DHBW-Service-App.git
synced 2024-11-01 00:43:58 +00:00
✨ Adding option to hide RaPla events
- Also made changes to home view, lecture plan list view, and added lecture plan item view
This commit is contained in:
parent
ea0b759007
commit
b8c6d44000
|
@ -31,6 +31,7 @@
|
||||||
CDDCF493259203390027CDC5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDDCF495259203390027CDC5 /* Localizable.strings */; };
|
CDDCF493259203390027CDC5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDDCF495259203390027CDC5 /* Localizable.strings */; };
|
||||||
CDDCF4A2259203B40027CDC5 /* General.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDDCF4A4259203B40027CDC5 /* General.strings */; };
|
CDDCF4A2259203B40027CDC5 /* General.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDDCF4A4259203B40027CDC5 /* General.strings */; };
|
||||||
CDEA70B225C6054F001CFE28 /* LecturePlanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */; };
|
CDEA70B225C6054F001CFE28 /* LecturePlanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */; };
|
||||||
|
CDEA70C025C85999001CFE28 /* LecturePlanItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEA70BF25C85999001CFE28 /* LecturePlanItem.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -83,6 +84,7 @@
|
||||||
CDDCF4A3259203B40027CDC5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/General.strings; sourceTree = "<group>"; };
|
CDDCF4A3259203B40027CDC5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/General.strings; sourceTree = "<group>"; };
|
||||||
CDDCF4A8259203B80027CDC5 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/General.strings; sourceTree = "<group>"; };
|
CDDCF4A8259203B80027CDC5 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/General.strings; sourceTree = "<group>"; };
|
||||||
CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LecturePlanList.swift; sourceTree = "<group>"; };
|
CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LecturePlanList.swift; sourceTree = "<group>"; };
|
||||||
|
CDEA70BF25C85999001CFE28 /* LecturePlanItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LecturePlanItem.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -257,6 +259,7 @@
|
||||||
CDCD721925912E1200FBF2F5 /* HomeView.swift */,
|
CDCD721925912E1200FBF2F5 /* HomeView.swift */,
|
||||||
CDD39B4A259A64150078D05F /* SettingsMain.swift */,
|
CDD39B4A259A64150078D05F /* SettingsMain.swift */,
|
||||||
CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */,
|
CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */,
|
||||||
|
CDEA70BF25C85999001CFE28 /* LecturePlanItem.swift */,
|
||||||
CD730A33259A85F500E0BB69 /* SettingsSubViews */,
|
CD730A33259A85F500E0BB69 /* SettingsSubViews */,
|
||||||
);
|
);
|
||||||
path = Tabs;
|
path = Tabs;
|
||||||
|
@ -441,6 +444,7 @@
|
||||||
CD9FAB83258EC60200D6D0C5 /* ContentView.swift in Sources */,
|
CD9FAB83258EC60200D6D0C5 /* ContentView.swift in Sources */,
|
||||||
CDCD72242591316500FBF2F5 /* LocalSettings.swift in Sources */,
|
CDCD72242591316500FBF2F5 /* LocalSettings.swift in Sources */,
|
||||||
CD8555BE25C47AE500C4ACD6 /* RaPlaFetcher.swift in Sources */,
|
CD8555BE25C47AE500C4ACD6 /* RaPlaFetcher.swift in Sources */,
|
||||||
|
CDEA70C025C85999001CFE28 /* LecturePlanItem.swift in Sources */,
|
||||||
CD8555C325C47B5300C4ACD6 /* ApiService.swift in Sources */,
|
CD8555C325C47B5300C4ACD6 /* ApiService.swift in Sources */,
|
||||||
CD9FAB8D258EC60600D6D0C5 /* DHBW_Service.xcdatamodeld in Sources */,
|
CD9FAB8D258EC60600D6D0C5 /* DHBW_Service.xcdatamodeld in Sources */,
|
||||||
CDCD721A25912E1200FBF2F5 /* HomeView.swift in Sources */,
|
CDCD721A25912E1200FBF2F5 /* HomeView.swift in Sources */,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<attribute name="category" optional="YES" attributeType="String"/>
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
<attribute name="descr" optional="YES" attributeType="String"/>
|
<attribute name="descr" optional="YES" attributeType="String"/>
|
||||||
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="isHidden" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="location" optional="YES" attributeType="String"/>
|
<attribute name="location" optional="YES" attributeType="String"/>
|
||||||
<attribute name="startDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="startDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="summary" optional="YES" attributeType="String"/>
|
<attribute name="summary" optional="YES" attributeType="String"/>
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||||
<element name="RaPlaEvent" positionX="-63" positionY="9" width="128" height="134"/>
|
<element name="RaPlaEvent" positionX="-63" positionY="9" width="128" height="149"/>
|
||||||
<element name="User" positionX="-63" positionY="-9" width="128" height="74"/>
|
<element name="User" positionX="-63" positionY="-9" width="128" height="74"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -134,6 +134,9 @@ class RaPlaFetcher {
|
||||||
} else {
|
} else {
|
||||||
let entity = NSEntityDescription.entity(forEntityName: "RaPlaEvent", in: PersistenceController.shared.context)!
|
let entity = NSEntityDescription.entity(forEntityName: "RaPlaEvent", in: PersistenceController.shared.context)!
|
||||||
evt = NSManagedObject(entity: entity, insertInto: PersistenceController.shared.context)
|
evt = NSManagedObject(entity: entity, insertInto: PersistenceController.shared.context)
|
||||||
|
|
||||||
|
// Set default values for new object
|
||||||
|
evt.setValue(false, forKey: "isHidden")
|
||||||
}
|
}
|
||||||
evt.setValue(event.startDate, forKey: "startDate")
|
evt.setValue(event.startDate, forKey: "startDate")
|
||||||
evt.setValue(event.endDate, forKey: "endDate")
|
evt.setValue(event.endDate, forKey: "endDate")
|
||||||
|
@ -150,7 +153,6 @@ class RaPlaFetcher {
|
||||||
// Locally stored event does not exist in RaPla anymore, delete it
|
// Locally stored event does not exist in RaPla anymore, delete it
|
||||||
let evt = existingEventsDict[localUid]
|
let evt = existingEventsDict[localUid]
|
||||||
PersistenceController.shared.context.delete(evt!)
|
PersistenceController.shared.context.delete(evt!)
|
||||||
print("Deleted " + localUid)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,16 @@ import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
class UtilityFunctions {
|
class UtilityFunctions {
|
||||||
public class func getCoreDataObject(entity: String, sortDescriptors: [NSSortDescriptor]) -> [NSManagedObject]{
|
public class func getCoreDataObject(entity: String, sortDescriptors: [NSSortDescriptor] = [], searchPredicate: NSPredicate? = nil) -> [NSManagedObject]{
|
||||||
let managedContext =
|
let managedContext =
|
||||||
PersistenceController.shared.context
|
PersistenceController.shared.context
|
||||||
|
|
||||||
let fetchRequest =
|
let fetchRequest =
|
||||||
NSFetchRequest<NSManagedObject>(entityName: entity)
|
NSFetchRequest<NSManagedObject>(entityName: entity)
|
||||||
fetchRequest.sortDescriptors = sortDescriptors
|
fetchRequest.sortDescriptors = sortDescriptors
|
||||||
|
if(searchPredicate != nil) {
|
||||||
|
fetchRequest.predicate = searchPredicate
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
return try managedContext.fetch(fetchRequest)
|
return try managedContext.fetch(fetchRequest)
|
||||||
|
|
|
@ -13,8 +13,12 @@ struct HomeView: View {
|
||||||
@State private var name: String = ""
|
@State private var name: String = ""
|
||||||
@State private var course: String = ""
|
@State private var course: String = ""
|
||||||
@State private var director: String = ""
|
@State private var director: String = ""
|
||||||
|
@State private var todaysEvents: [NSManagedObject] = []
|
||||||
|
@State private var tomorrowsEvents: [NSManagedObject] = []
|
||||||
|
@State private var upcomingExams: [NSManagedObject] = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Text("name".localized(tableName: "General", plural: false) + ": ")
|
Text("name".localized(tableName: "General", plural: false) + ": ")
|
||||||
|
@ -28,15 +32,78 @@ struct HomeView: View {
|
||||||
Text("director".localized(tableName: "General", plural: false) + ": ")
|
Text("director".localized(tableName: "General", plural: false) + ": ")
|
||||||
Text(self.director)
|
Text(self.director)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upcoming events section
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("Today's events")
|
||||||
|
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
VStack {
|
||||||
|
Text("Evt 1")
|
||||||
|
Text("Evt 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(Color.blue, lineWidth: 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("Tomorrow's events")
|
||||||
|
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
VStack {
|
||||||
|
Text("Evt 1")
|
||||||
|
Text("Evt 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(Color.blue, lineWidth: 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exams section
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("Upcoming exams")
|
||||||
|
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
VStack {
|
||||||
|
ForEach(upcomingExams, id: \.self) { exam in
|
||||||
|
Text(exam.value(forKey: "summary") as! String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(Color.red, lineWidth: 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitle(Text("Home"))
|
||||||
}.onAppear{
|
}.onAppear{
|
||||||
self.readFromCoreData()
|
self.readFromCoreData()
|
||||||
|
self.upcomingExams = getUpcomingExams()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HomeView{
|
extension HomeView{
|
||||||
func readFromCoreData() {
|
func readFromCoreData() {
|
||||||
let fetchedData = UtilityFunctions.getCoreDataObject(entity: "User", sortDescriptors: [])
|
let fetchedData = UtilityFunctions.getCoreDataObject(entity: "User")
|
||||||
|
|
||||||
if(!fetchedData.isEmpty) {
|
if(!fetchedData.isEmpty) {
|
||||||
let user = fetchedData[0]
|
let user = fetchedData[0]
|
||||||
|
@ -45,6 +112,32 @@ extension HomeView{
|
||||||
self.director = user.value(forKey: "director") as! String
|
self.director = user.value(forKey: "director") as! String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTodaysEvents() -> [NSManagedObject] {
|
||||||
|
// let searchPredicate = NSPredicate(format: "(category == 'Lehrveranstaltung') AND (startDate = %@)", Date())
|
||||||
|
// return Array(UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", searchPredicate: searchPredicate)[0...1])
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTomorrowsEvents() -> [NSManagedObject] {
|
||||||
|
// let searchPredicate = NSPredicate(format: "(category == 'Lehrveranstaltung') AND (startDate = %@)", Date().)
|
||||||
|
// return Array(UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", searchPredicate: searchPredicate)[0...1])
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUpcomingExams() -> [NSManagedObject] {
|
||||||
|
let searchPredicate = NSPredicate(format: "category == %@", "Prüfung")
|
||||||
|
let hiddenPredicate = NSPredicate(format: "isHidden == NO")
|
||||||
|
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [searchPredicate, hiddenPredicate])
|
||||||
|
let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: true)
|
||||||
|
let sortDescriptors = [sectionSortDescriptor]
|
||||||
|
let events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors, searchPredicate: compoundPredicate)
|
||||||
|
if(events.count > 0) {
|
||||||
|
return Array(events[0...min(1, events.count)])
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomeView_Previews: PreviewProvider {
|
struct HomeView_Previews: PreviewProvider {
|
||||||
|
|
57
DHBW-Service/Views/Tabs/LecturePlanItem.swift
Normal file
57
DHBW-Service/Views/Tabs/LecturePlanItem.swift
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
// LecturePlanItem.swift
|
||||||
|
// DHBW-Service
|
||||||
|
//
|
||||||
|
// Created by Patrick Müller on 01.02.21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct LecturePlanItem: View {
|
||||||
|
@State var event: NSManagedObject
|
||||||
|
@State var isHidden = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text(event.value(forKey: "summary") as! String)
|
||||||
|
Button(action: {
|
||||||
|
event.setValue(!isHidden, forKey: "isHidden")
|
||||||
|
self.isHidden = !isHidden
|
||||||
|
PersistenceController.shared.save()
|
||||||
|
}){
|
||||||
|
if(self.isHidden){
|
||||||
|
Text("Show")
|
||||||
|
} else {
|
||||||
|
Text("Hide")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.background(Color.blue)
|
||||||
|
.cornerRadius(15)
|
||||||
|
}
|
||||||
|
.onAppear{
|
||||||
|
self.isHidden = event.value(forKey: "isHidden") as! Bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LecturePlanItem_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LecturePlanItem(event: getPreviewEvent())
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.environmentObject(getFirstOpening())
|
||||||
|
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getFirstOpening() -> LocalSettings {
|
||||||
|
let settings = LocalSettings();
|
||||||
|
settings.isFirstOpening = false;
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getPreviewEvent() -> NSManagedObject {
|
||||||
|
return UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: [])[0]
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,30 +13,52 @@ struct LecturePlanList: View {
|
||||||
@State private var sortingAscending = true
|
@State private var sortingAscending = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
NavigationView() {
|
||||||
Button(action: {
|
|
||||||
// This is obviously bullshit, but it could be used to sort afer summary or smth like that
|
|
||||||
|
|
||||||
self.sortingAscending = !self.sortingAscending
|
|
||||||
let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: sortingAscending)
|
|
||||||
let sortDescriptors = [sectionSortDescriptor]
|
|
||||||
self.events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors)
|
|
||||||
}){
|
|
||||||
Text("Switch order")
|
|
||||||
}
|
|
||||||
List {
|
List {
|
||||||
ForEach(events, id: \.self) { event in
|
ForEach(events, id: \.self) { event in
|
||||||
|
NavigationLink(destination: LecturePlanItem(event: event)){
|
||||||
HStack {
|
HStack {
|
||||||
Text(formatDate(date: event.value(forKeyPath: "startDate") as! Date))
|
Text(formatDate(date: event.value(forKeyPath: "startDate") as! Date))
|
||||||
.foregroundColor(getEventForegroundColor(for: event))
|
.foregroundColor(getEventForegroundColor(for: event))
|
||||||
Text(event.value(forKeyPath: "summary") as! String)
|
Text(event.value(forKeyPath: "summary") as! String)
|
||||||
.foregroundColor(getEventForegroundColor(for: event))
|
.foregroundColor(getEventForegroundColor(for: event))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if(event.value(forKey: "isHidden") as! Bool) {
|
||||||
|
Image(systemName: "eye.slash")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "eye")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// When an event gets updated from child view, reload it here as this will not trigger the onAppear() function
|
||||||
|
.onReceive(event.objectWillChange, perform: { _ in
|
||||||
|
let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: true)
|
||||||
|
let sortDescriptors = [sectionSortDescriptor]
|
||||||
|
self.events = []
|
||||||
|
self.events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitle(Text("Lectures"))
|
||||||
|
// .navigationBarItems(trailing: {
|
||||||
|
// Button(action: {
|
||||||
|
// // This is obviously bullshit, but it could be used to sort afer summary or smth like that
|
||||||
|
//
|
||||||
|
// self.sortingAscending = !self.sortingAscending
|
||||||
|
// let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: sortingAscending)
|
||||||
|
// let sortDescriptors = [sectionSortDescriptor]
|
||||||
|
// self.events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors)
|
||||||
|
// }){
|
||||||
|
// Text("Switch order")
|
||||||
|
// }
|
||||||
|
// })
|
||||||
}.onAppear{
|
}.onAppear{
|
||||||
let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: true)
|
let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: true)
|
||||||
let sortDescriptors = [sectionSortDescriptor]
|
let sortDescriptors = [sectionSortDescriptor]
|
||||||
|
self.events = []
|
||||||
self.events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors)
|
self.events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user