mirror of
https://github.com/taigrr/wails.git
synced 2026-04-17 04:05:12 -07:00
Compare commits
7 Commits
v2.0.0-alp
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e6a0f312 | ||
|
|
81e83fdf18 | ||
|
|
f9b79d24f8 | ||
|
|
0599a47bfe | ||
|
|
817c55d318 | ||
|
|
14146c8c0c | ||
|
|
18adac20d4 |
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var version = "v2.0.0-alpha.44"
|
var version = "v2.0.0-alpha.47"
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ struct hashmap_s dialogIconCache;
|
|||||||
// Dispatch Method
|
// Dispatch Method
|
||||||
typedef void (^dispatchMethod)(void);
|
typedef void (^dispatchMethod)(void);
|
||||||
|
|
||||||
|
TrayMenuStore *TrayMenuStoreSingleton;
|
||||||
|
|
||||||
// dispatch will execute the given `func` pointer
|
// dispatch will execute the given `func` pointer
|
||||||
void dispatch(dispatchMethod func) {
|
void dispatch(dispatchMethod func) {
|
||||||
dispatch_async(dispatch_get_main_queue(), func);
|
dispatch_async(dispatch_get_main_queue(), func);
|
||||||
@@ -55,6 +57,9 @@ void filelog(const char *message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The delegate class for tray menus
|
||||||
|
Class trayMenuDelegateClass;
|
||||||
|
|
||||||
// Utility function to visualise a hashmap
|
// Utility function to visualise a hashmap
|
||||||
void dumpHashmap(const char *name, struct hashmap_s *hashmap) {
|
void dumpHashmap(const char *name, struct hashmap_s *hashmap) {
|
||||||
printf("%s = { ", name);
|
printf("%s = { ", name);
|
||||||
@@ -127,9 +132,6 @@ struct Application {
|
|||||||
// Menu
|
// Menu
|
||||||
Menu *applicationMenu;
|
Menu *applicationMenu;
|
||||||
|
|
||||||
// Tray
|
|
||||||
TrayMenuStore* trayMenuStore;
|
|
||||||
|
|
||||||
// Context Menus
|
// Context Menus
|
||||||
ContextMenuStore *contextMenuStore;
|
ContextMenuStore *contextMenuStore;
|
||||||
|
|
||||||
@@ -477,7 +479,7 @@ void DestroyApplication(struct Application *app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete the tray menu store
|
// Delete the tray menu store
|
||||||
DeleteTrayMenuStore(app->trayMenuStore);
|
DeleteTrayMenuStore(TrayMenuStoreSingleton);
|
||||||
|
|
||||||
// Delete the context menu store
|
// Delete the context menu store
|
||||||
DeleteContextMenuStore(app->contextMenuStore);
|
DeleteContextMenuStore(app->contextMenuStore);
|
||||||
@@ -1044,7 +1046,7 @@ void AddTrayMenu(struct Application *app, const char *trayMenuJSON) {
|
|||||||
// Guard against calling during shutdown
|
// Guard against calling during shutdown
|
||||||
if( app->shuttingDown ) return;
|
if( app->shuttingDown ) return;
|
||||||
|
|
||||||
AddTrayMenuToStore(app->trayMenuStore, trayMenuJSON);
|
AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
|
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
|
||||||
@@ -1053,13 +1055,13 @@ void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
|
|||||||
if( app->shuttingDown ) return;
|
if( app->shuttingDown ) return;
|
||||||
|
|
||||||
ON_MAIN_THREAD(
|
ON_MAIN_THREAD(
|
||||||
UpdateTrayMenuInStore(app->trayMenuStore, trayMenuJSON);
|
UpdateTrayMenuInStore(TrayMenuStoreSingleton, trayMenuJSON);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeleteTrayMenuByID(struct Application *app, const char *id) {
|
void DeleteTrayMenuByID(struct Application *app, const char *id) {
|
||||||
ON_MAIN_THREAD(
|
ON_MAIN_THREAD(
|
||||||
DeleteTrayMenuInStore(app->trayMenuStore, id);
|
DeleteTrayMenuInStore(TrayMenuStoreSingleton, id);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1068,7 +1070,7 @@ void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
|
|||||||
if( app->shuttingDown ) return;
|
if( app->shuttingDown ) return;
|
||||||
|
|
||||||
ON_MAIN_THREAD(
|
ON_MAIN_THREAD(
|
||||||
UpdateTrayMenuLabelInStore(app->trayMenuStore, JSON);
|
UpdateTrayMenuLabelInStore(TrayMenuStoreSingleton, JSON);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,7 +1166,7 @@ void getURL(id self, SEL selector, id event, id replyEvent) {
|
|||||||
|
|
||||||
|
|
||||||
void createDelegate(struct Application *app) {
|
void createDelegate(struct Application *app) {
|
||||||
// Define delegate
|
// Define delegate
|
||||||
Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0);
|
Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0);
|
||||||
bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate"));
|
bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate"));
|
||||||
class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@");
|
class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@");
|
||||||
@@ -1661,6 +1663,35 @@ void processUserDialogIcons(struct Application *app) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TrayMenuWillOpen(id self, SEL selector, id menu) {
|
||||||
|
// Extract tray menu id from menu
|
||||||
|
id trayMenuIDStr = objc_getAssociatedObject(menu, "trayMenuID");
|
||||||
|
const char* trayMenuID = cstr(trayMenuIDStr);
|
||||||
|
const char *message = concat("Mo", trayMenuID);
|
||||||
|
messageFromWindowCallback(message);
|
||||||
|
MEMFREE(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrayMenuDidClose(id self, SEL selector, id menu) {
|
||||||
|
// Extract tray menu id from menu
|
||||||
|
id trayMenuIDStr = objc_getAssociatedObject(menu, "trayMenuID");
|
||||||
|
const char* trayMenuID = cstr(trayMenuIDStr);
|
||||||
|
const char *message = concat("Mc", trayMenuID);
|
||||||
|
messageFromWindowCallback(message);
|
||||||
|
MEMFREE(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createTrayMenuDelegate() {
|
||||||
|
// Define delegate
|
||||||
|
trayMenuDelegateClass = objc_allocateClassPair((Class) c("NSObject"), "MenuDelegate", 0);
|
||||||
|
class_addProtocol(trayMenuDelegateClass, objc_getProtocol("NSMenuDelegate"));
|
||||||
|
class_addMethod(trayMenuDelegateClass, s("menuWillOpen:"), (IMP) TrayMenuWillOpen, "v@:@");
|
||||||
|
class_addMethod(trayMenuDelegateClass, s("menuDidClose:"), (IMP) TrayMenuDidClose, "v@:@");
|
||||||
|
|
||||||
|
// Script handler
|
||||||
|
objc_registerClassPair(trayMenuDelegateClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void Run(struct Application *app, int argc, char **argv) {
|
void Run(struct Application *app, int argc, char **argv) {
|
||||||
|
|
||||||
@@ -1673,6 +1704,9 @@ void Run(struct Application *app, int argc, char **argv) {
|
|||||||
// Define delegate
|
// Define delegate
|
||||||
createDelegate(app);
|
createDelegate(app);
|
||||||
|
|
||||||
|
// Define tray delegate
|
||||||
|
createTrayMenuDelegate();
|
||||||
|
|
||||||
// Create the main window
|
// Create the main window
|
||||||
createMainWindow(app);
|
createMainWindow(app);
|
||||||
|
|
||||||
@@ -1843,7 +1877,7 @@ void Run(struct Application *app, int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup initial trays
|
// Setup initial trays
|
||||||
ShowTrayMenusInStore(app->trayMenuStore);
|
ShowTrayMenusInStore(TrayMenuStoreSingleton);
|
||||||
|
|
||||||
// Process dialog icons
|
// Process dialog icons
|
||||||
processUserDialogIcons(app);
|
processUserDialogIcons(app);
|
||||||
@@ -1920,7 +1954,7 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
|
|||||||
result->applicationMenu = NULL;
|
result->applicationMenu = NULL;
|
||||||
|
|
||||||
// Tray
|
// Tray
|
||||||
result->trayMenuStore = NewTrayMenuStore();
|
TrayMenuStoreSingleton = NewTrayMenuStore();
|
||||||
|
|
||||||
// Context Menus
|
// Context Menus
|
||||||
result->contextMenuStore = NewContextMenuStore();
|
result->contextMenuStore = NewContextMenuStore();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "traymenu_darwin.h"
|
#include "traymenu_darwin.h"
|
||||||
#include "trayicons.h"
|
#include "trayicons.h"
|
||||||
|
|
||||||
|
extern Class trayMenuDelegateClass;
|
||||||
|
|
||||||
// A cache for all our tray menu icons
|
// A cache for all our tray menu icons
|
||||||
// Global because it's a singleton
|
// Global because it's a singleton
|
||||||
struct hashmap_s trayIconCache;
|
struct hashmap_s trayIconCache;
|
||||||
@@ -35,6 +37,8 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
|
|||||||
// Create the menu
|
// Create the menu
|
||||||
result->menu = NewMenu(processedMenu);
|
result->menu = NewMenu(processedMenu);
|
||||||
|
|
||||||
|
result->delegate = NULL;
|
||||||
|
|
||||||
// Init tray status bar item
|
// Init tray status bar item
|
||||||
result->statusbaritem = NULL;
|
result->statusbaritem = NULL;
|
||||||
|
|
||||||
@@ -78,12 +82,20 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
|
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
|
||||||
|
|
||||||
|
// If we don't have the image in the icon cache then assume it's base64 encoded image data
|
||||||
|
if (trayImage == NULL) {
|
||||||
|
id data = ALLOC("NSData");
|
||||||
|
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
|
||||||
|
trayImage = ALLOC("NSImage");
|
||||||
|
msg(trayImage, s("initWithData:"), imageData);
|
||||||
|
}
|
||||||
|
|
||||||
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
|
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
|
||||||
msg(statusBarButton, s("setImage:"), trayImage);
|
msg(statusBarButton, s("setImage:"), trayImage);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void ShowTrayMenu(TrayMenu* trayMenu) {
|
void ShowTrayMenu(TrayMenu* trayMenu) {
|
||||||
|
|
||||||
// Create a status bar item if we don't have one
|
// Create a status bar item if we don't have one
|
||||||
@@ -91,7 +103,6 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
|
|||||||
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
|
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
|
||||||
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
|
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
|
||||||
msg(trayMenu->statusbaritem, s("retain"));
|
msg(trayMenu->statusbaritem, s("retain"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
|
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
|
||||||
@@ -105,6 +116,16 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
|
|||||||
|
|
||||||
// Update the menu
|
// Update the menu
|
||||||
id menu = GetMenu(trayMenu->menu);
|
id menu = GetMenu(trayMenu->menu);
|
||||||
|
objc_setAssociatedObject(menu, "trayMenuID", str(trayMenu->ID), OBJC_ASSOCIATION_ASSIGN);
|
||||||
|
|
||||||
|
// Create delegate
|
||||||
|
id trayMenuDelegate = msg((id)trayMenuDelegateClass, s("new"));
|
||||||
|
msg(menu, s("setDelegate:"), trayMenuDelegate);
|
||||||
|
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
|
||||||
|
|
||||||
|
// Create menu delegate
|
||||||
|
trayMenu->delegate = trayMenuDelegate;
|
||||||
|
|
||||||
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
|
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +174,10 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
|
|||||||
trayMenu->statusbaritem = NULL;
|
trayMenu->statusbaritem = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( trayMenu->delegate != NULL ) {
|
||||||
|
msg(trayMenu->delegate, s("release"));
|
||||||
|
}
|
||||||
|
|
||||||
// Free the tray menu memory
|
// Free the tray menu memory
|
||||||
MEMFREE(trayMenu);
|
MEMFREE(trayMenu);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ typedef struct {
|
|||||||
|
|
||||||
JsonNode* processedJSON;
|
JsonNode* processedJSON;
|
||||||
|
|
||||||
|
id delegate;
|
||||||
|
|
||||||
} TrayMenu;
|
} TrayMenu;
|
||||||
|
|
||||||
TrayMenu* NewTrayMenu(const char *trayJSON);
|
TrayMenu* NewTrayMenu(const char *trayJSON);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#ifndef TRAYMENUSTORE_DARWIN_H
|
#ifndef TRAYMENUSTORE_DARWIN_H
|
||||||
#define TRAYMENUSTORE_DARWIN_H
|
#define TRAYMENUSTORE_DARWIN_H
|
||||||
|
|
||||||
|
#include "traymenu_darwin.h"
|
||||||
|
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -26,6 +28,8 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
|
|||||||
void ShowTrayMenusInStore(TrayMenuStore* store);
|
void ShowTrayMenusInStore(TrayMenuStore* store);
|
||||||
void DeleteTrayMenuStore(TrayMenuStore* store);
|
void DeleteTrayMenuStore(TrayMenuStore* store);
|
||||||
|
|
||||||
|
TrayMenu* GetTrayMenuByID(TrayMenuStore* store, const char* menuID);
|
||||||
|
|
||||||
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
|
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
|
||||||
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);
|
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type TrayMenu struct {
|
|||||||
menuItemMap *MenuItemMap
|
menuItemMap *MenuItemMap
|
||||||
menu *menu.Menu
|
menu *menu.Menu
|
||||||
ProcessedMenu *WailsMenu
|
ProcessedMenu *WailsMenu
|
||||||
|
trayMenu *menu.TrayMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TrayMenu) AsJSON() (string, error) {
|
func (t *TrayMenu) AsJSON() (string, error) {
|
||||||
@@ -46,6 +47,7 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
|
|||||||
Icon: trayMenu.Icon,
|
Icon: trayMenu.Icon,
|
||||||
menu: trayMenu.Menu,
|
menu: trayMenu.Menu,
|
||||||
menuItemMap: NewMenuItemMap(),
|
menuItemMap: NewMenuItemMap(),
|
||||||
|
trayMenu: trayMenu,
|
||||||
}
|
}
|
||||||
|
|
||||||
result.menuItemMap.AddMenu(trayMenu.Menu)
|
result.menuItemMap.AddMenu(trayMenu.Menu)
|
||||||
@@ -54,6 +56,28 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) OnTrayMenuOpen(id string) {
|
||||||
|
trayMenu, ok := m.trayMenus[id]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if trayMenu.trayMenu.OnOpen == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go trayMenu.trayMenu.OnOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) OnTrayMenuClose(id string) {
|
||||||
|
trayMenu, ok := m.trayMenus[id]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if trayMenu.trayMenu.OnClose == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go trayMenu.trayMenu.OnClose()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
|
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
|
||||||
newTrayMenu := NewTrayMenu(trayMenu)
|
newTrayMenu := NewTrayMenu(trayMenu)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ func menuMessageParser(message string) (*parsedMessage, error) {
|
|||||||
callbackid := message[2:]
|
callbackid := message[2:]
|
||||||
topic = "menu:clicked"
|
topic = "menu:clicked"
|
||||||
data = callbackid
|
data = callbackid
|
||||||
|
case 'o':
|
||||||
|
callbackid := message[2:]
|
||||||
|
topic = "menu:ontrayopen"
|
||||||
|
data = callbackid
|
||||||
|
case 'c':
|
||||||
|
callbackid := message[2:]
|
||||||
|
topic = "menu:ontrayclose"
|
||||||
|
data = callbackid
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid menu message: %s", message)
|
return nil, fmt.Errorf("invalid menu message: %s", message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ func (m *Menu) Start() error {
|
|||||||
splitTopic := strings.Split(menuMessage.Topic(), ":")
|
splitTopic := strings.Split(menuMessage.Topic(), ":")
|
||||||
menuMessageType := splitTopic[1]
|
menuMessageType := splitTopic[1]
|
||||||
switch menuMessageType {
|
switch menuMessageType {
|
||||||
|
case "ontrayopen":
|
||||||
|
trayID := menuMessage.Data().(string)
|
||||||
|
m.menuManager.OnTrayMenuOpen(trayID)
|
||||||
|
case "ontrayclose":
|
||||||
|
trayID := menuMessage.Data().(string)
|
||||||
|
m.menuManager.OnTrayMenuClose(trayID)
|
||||||
case "clicked":
|
case "clicked":
|
||||||
if len(splitTopic) != 2 {
|
if len(splitTopic) != 2 {
|
||||||
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
|
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ func (u *URL) Start() error {
|
|||||||
u.wg.Done()
|
u.wg.Done()
|
||||||
return
|
return
|
||||||
case urlMessage := <-u.urlChannel:
|
case urlMessage := <-u.urlChannel:
|
||||||
|
// Guard against nil messages
|
||||||
|
if urlMessage == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
messageType := strings.TrimPrefix(urlMessage.Topic(), "url:")
|
messageType := strings.TrimPrefix(urlMessage.Topic(), "url:")
|
||||||
switch messageType {
|
switch messageType {
|
||||||
case "handler":
|
case "handler":
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ var modifierMap = map[string]Modifier{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseModifier(text string) (*Modifier, error) {
|
func parseModifier(text string) (*Modifier, error) {
|
||||||
result, valid := modifierMap[text]
|
lowertext := strings.ToLower(text)
|
||||||
|
result, valid := modifierMap[lowertext]
|
||||||
if !valid {
|
if !valid {
|
||||||
return nil, fmt.Errorf("'%s' is not a valid modifier", text)
|
return nil, fmt.Errorf("'%s' is not a valid modifier", text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,10 @@ type TrayMenu struct {
|
|||||||
|
|
||||||
// Menu is the initial menu we wish to use for the tray
|
// Menu is the initial menu we wish to use for the tray
|
||||||
Menu *Menu
|
Menu *Menu
|
||||||
|
|
||||||
|
// OnOpen is called when the Menu is opened
|
||||||
|
OnOpen func()
|
||||||
|
|
||||||
|
// OnClose is called when the Menu is closed
|
||||||
|
OnClose func()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user