Set and delete our avatar from the Self menu and the avatar's menu
As in TeamSpeak 3, Self offers "Set avatar…", which picks an image and uploads it, and "Delete avatar" while we have one; the old avatar dialog is gone. Setting (and stealing) is offered only while the server grants i_client_max_avatar_filesize. Right-clicking our own avatar in the info panel offers the same two beside saving it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
import com.ts3client.net.avatar.Avatar;
|
||||
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.Component;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
/** Sets, takes and deletes our avatar on one server, reporting failures in a dialog. */
|
||||
final class AvatarActions {
|
||||
|
||||
/** Where the last avatar was picked from, for every server alike. */
|
||||
private static File lastDirectory;
|
||||
|
||||
private final Component owner;
|
||||
private final TeamspeakConnection conn;
|
||||
|
||||
AvatarActions(Component owner, TeamspeakConnection conn) {
|
||||
this.owner = owner;
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
/** Asks for an image and uploads it as our avatar. */
|
||||
void choose() {
|
||||
if (!conn.isConnected()) return;
|
||||
JFileChooser chooser = new JFileChooser(lastDirectory);
|
||||
chooser.setDialogTitle("Select your avatar");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("Images", "png", "jpg", "jpeg", "gif", "bmp"));
|
||||
if (chooser.showOpenDialog(owner) != JFileChooser.APPROVE_OPTION) return;
|
||||
File file = chooser.getSelectedFile();
|
||||
lastDirectory = file.getParentFile();
|
||||
conn.setAvatar(file, error -> fail("Error uploading avatar: ", error));
|
||||
}
|
||||
|
||||
/** Uploads someone else's avatar as ours. */
|
||||
void steal(Avatar avatar) {
|
||||
if (!conn.isConnected()) return;
|
||||
File file;
|
||||
try {
|
||||
file = Files.createTempFile("avatar", "." + avatar.format().extension).toFile();
|
||||
Files.write(file.toPath(), avatar.data());
|
||||
} catch (IOException e) {
|
||||
fail("Error uploading avatar: ", e.getMessage());
|
||||
return;
|
||||
}
|
||||
conn.setAvatar(file, error -> {
|
||||
file.delete();
|
||||
fail("Error uploading avatar: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
void delete() {
|
||||
if (!conn.isConnected()) return;
|
||||
conn.removeAvatar(error -> fail("Error deleting avatar: ", error));
|
||||
}
|
||||
|
||||
/** Shows {@code error}, if there is one; callable from any thread. */
|
||||
private void fail(String what, String error) {
|
||||
if (error == null) return;
|
||||
SwingUtilities.invokeLater(() ->
|
||||
JOptionPane.showMessageDialog(owner, what + error, "Error", JOptionPane.ERROR_MESSAGE));
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
import com.ts3client.net.avatar.Avatar;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Self → Set avatar: shows the avatar we currently publish on this server and lets the
|
||||
* user replace it with an image file (PNG, JPEG, BMP or GIF — animated GIFs are kept
|
||||
* as they are, so they animate for everyone) or withdraw it.
|
||||
*/
|
||||
final class AvatarDialog extends JDialog {
|
||||
|
||||
private final TeamspeakConnection conn;
|
||||
private final AvatarView preview = new AvatarView(200);
|
||||
private final JLabel status = new JLabel(" ");
|
||||
private final JButton remove = new JButton("Remove avatar", Icons.of("DELETE_AVATAR"));
|
||||
private final JButton choose = new JButton("Select image…", Icons.of("UPLOAD_AVATAR"));
|
||||
private File lastDirectory;
|
||||
|
||||
AvatarDialog(Frame owner, TeamspeakConnection conn) {
|
||||
super(owner, "Set Avatar", true);
|
||||
this.conn = conn;
|
||||
|
||||
JPanel picture = new JPanel(new BorderLayout());
|
||||
picture.setBorder(BorderFactory.createCompoundBorder(
|
||||
new ScaledEmptyBorder(8, 8, 4, 8),
|
||||
BorderFactory.createLineBorder(Theme.border())));
|
||||
picture.add(preview, BorderLayout.CENTER);
|
||||
|
||||
JLabel hint = new JLabel("<html>Images up to " + (conn.maxAvatarBytes() / 1024)
|
||||
+ " KiB may be uploaded to this server. Animated GIFs stay animated.</html>");
|
||||
hint.setFont(Theme.uiSmall());
|
||||
hint.setBorder(new ScaledEmptyBorder(0, 8, 0, 8));
|
||||
status.setFont(Theme.uiSmall());
|
||||
status.setBorder(new ScaledEmptyBorder(2, 8, 0, 8));
|
||||
|
||||
JPanel text = new JPanel();
|
||||
text.setLayout(new BoxLayout(text, BoxLayout.Y_AXIS));
|
||||
text.add(hint);
|
||||
text.add(status);
|
||||
|
||||
JPanel buttons = new JPanel();
|
||||
buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
|
||||
buttons.setBorder(new ScaledEmptyBorder(6, 8, 8, 8));
|
||||
choose.addActionListener(e -> chooseFile());
|
||||
remove.addActionListener(e -> removeAvatar());
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(e -> dispose());
|
||||
buttons.add(choose);
|
||||
buttons.add(Box.createHorizontalStrut(UIScale.scale(4)));
|
||||
buttons.add(remove);
|
||||
buttons.add(Box.createHorizontalGlue());
|
||||
buttons.add(close);
|
||||
|
||||
JPanel south = new JPanel(new BorderLayout());
|
||||
south.add(text, BorderLayout.NORTH);
|
||||
south.add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(picture, BorderLayout.CENTER);
|
||||
getContentPane().add(south, BorderLayout.SOUTH);
|
||||
|
||||
showCurrent();
|
||||
Dialogs.closeOnEscape(this);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
pack();
|
||||
setResizable(false);
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
private void showCurrent() {
|
||||
ClientEntry self = conn.getModel().getClient(conn.getSelfClientId());
|
||||
Avatar avatar = conn.avatarOf(self);
|
||||
preview.setImage(avatar == null ? null : avatar.data());
|
||||
remove.setEnabled(avatar != null || (self != null && !self.avatarFlag.isEmpty()));
|
||||
status.setText(avatar == null ? "You have no avatar on this server." : " ");
|
||||
}
|
||||
|
||||
private void chooseFile() {
|
||||
JFileChooser chooser = new JFileChooser(lastDirectory);
|
||||
chooser.setDialogTitle("Select your avatar");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("Images", "png", "jpg", "jpeg", "gif", "bmp"));
|
||||
if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
|
||||
File file = chooser.getSelectedFile();
|
||||
lastDirectory = file.getParentFile();
|
||||
setBusy(true, "Uploading " + file.getName() + "…");
|
||||
conn.setAvatar(file, error -> SwingUtilities.invokeLater(() -> {
|
||||
setBusy(false, " ");
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, "Error uploading avatar: " + error,
|
||||
"Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
showCurrent();
|
||||
}));
|
||||
}
|
||||
|
||||
private void removeAvatar() {
|
||||
setBusy(true, "Removing avatar…");
|
||||
conn.removeAvatar(error -> SwingUtilities.invokeLater(() -> {
|
||||
setBusy(false, " ");
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, "Error removing avatar: " + error,
|
||||
"Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
showCurrent();
|
||||
}));
|
||||
}
|
||||
|
||||
private void setBusy(boolean busy, String message) {
|
||||
choose.setEnabled(!busy);
|
||||
remove.setEnabled(!busy);
|
||||
status.setText(message);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Toolkit;
|
||||
|
||||
/**
|
||||
* Paints an avatar scaled to fit, keeping its aspect ratio. The image is a Toolkit
|
||||
* image drawn with this component as its observer, so an animated GIF keeps playing:
|
||||
* each decoded frame triggers a repaint.
|
||||
*/
|
||||
final class AvatarView extends JComponent {
|
||||
|
||||
private final int boxSize;
|
||||
private Image image;
|
||||
private int imageWidth = -1;
|
||||
private int imageHeight = -1;
|
||||
|
||||
AvatarView(int boxSize) {
|
||||
this.boxSize = boxSize;
|
||||
setPreferredSize(UIScale.scale(new Dimension(boxSize, boxSize)));
|
||||
setMinimumSize(getPreferredSize());
|
||||
}
|
||||
|
||||
/** @param data the encoded image, or {@code null} to show nothing */
|
||||
void setImage(byte[] data) {
|
||||
if (image != null) image.flush();
|
||||
image = data == null ? null : Toolkit.getDefaultToolkit().createImage(data);
|
||||
imageWidth = imageHeight = -1;
|
||||
repaint();
|
||||
}
|
||||
|
||||
boolean hasImage() {
|
||||
return image != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
if (image == null) return;
|
||||
if (imageWidth < 0 || imageHeight < 0) {
|
||||
// Unknown until the header is decoded; the observer repaints once it is.
|
||||
imageWidth = image.getWidth(this);
|
||||
imageHeight = image.getHeight(this);
|
||||
if (imageWidth < 0 || imageHeight < 0) return;
|
||||
}
|
||||
double scale = Math.min(UIScale.getUserScaleFactor(), Math.min((double) getWidth() / imageWidth, (double) getHeight() / imageHeight));
|
||||
int w = Math.max(1, (int) Math.round(imageWidth * scale));
|
||||
int h = Math.max(1, (int) Math.round(imageHeight * scale));
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2.drawImage(image, (getWidth() - w) / 2, (getHeight() - h) / 2, w, h, this);
|
||||
} finally {
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
int boxSize() {
|
||||
return boxSize;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Read-only detail view for the server, channel or client selected, mirroring the
|
||||
@@ -64,6 +63,20 @@ public final class InfoPanel extends JPanel {
|
||||
void setPanelHidden(boolean hidden);
|
||||
}
|
||||
|
||||
/** What the avatar's menu does. */
|
||||
public interface AvatarHandler {
|
||||
/** Takes someone else's avatar as ours. */
|
||||
void steal(Avatar avatar);
|
||||
|
||||
/** Whether we may upload an avatar. */
|
||||
boolean canSet();
|
||||
|
||||
/** Asks for an image to upload as our avatar. */
|
||||
void set();
|
||||
|
||||
void delete();
|
||||
}
|
||||
|
||||
private static final int AVATAR_MIN_SIZE = 64;
|
||||
private static final int AVATAR_BOX_STEP = 16;
|
||||
/** Pictures in a chat tab have no panel to fit, so they get this much. */
|
||||
@@ -75,8 +88,7 @@ public final class InfoPanel extends JPanel {
|
||||
/** Floats over the bottom-right corner of the details, out of the text's way. */
|
||||
private final JLabel toggle = new JLabel("Show in chat tab");
|
||||
private DescriptionHandler descriptionHandler;
|
||||
/** Takes someone else's avatar as ours; null where that cannot be done. */
|
||||
private Consumer<Avatar> avatarStealer;
|
||||
private AvatarHandler avatarHandler;
|
||||
private ChannelNode shownChannel;
|
||||
private ClientEntry shownClient;
|
||||
/** The server's address while the server itself is shown. */
|
||||
@@ -219,8 +231,8 @@ public final class InfoPanel extends JPanel {
|
||||
this.descriptionHandler = handler;
|
||||
}
|
||||
|
||||
public void setAvatarStealer(Consumer<Avatar> stealer) {
|
||||
this.avatarStealer = stealer;
|
||||
public void setAvatarHandler(AvatarHandler handler) {
|
||||
this.avatarHandler = handler;
|
||||
}
|
||||
|
||||
/** Runs when the user closes the info chat tab; shows details in the panel again. */
|
||||
@@ -298,17 +310,23 @@ public final class InfoPanel extends JPanel {
|
||||
Avatar avatar = shownClient == null ? null : shownAvatar;
|
||||
if (avatar == null || !overAvatar(avatar, e.getPoint())) return;
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
JMenuItem save = new JMenuItem("Save…");
|
||||
save.addActionListener(a -> saveAvatar(avatar, "avatar_" + shownClient.nickname));
|
||||
menu.add(save);
|
||||
if (!shownClient.self && avatarStealer != null) {
|
||||
JMenuItem steal = new JMenuItem("Steal for avatar");
|
||||
steal.addActionListener(a -> avatarStealer.accept(avatar));
|
||||
menu.add(steal);
|
||||
String name = "avatar_" + shownClient.nickname;
|
||||
menu.add(item("Save…", null, () -> saveAvatar(avatar, name)));
|
||||
if (avatarHandler != null && !shownClient.self) {
|
||||
if (avatarHandler.canSet()) menu.add(item("Steal for avatar", null, () -> avatarHandler.steal(avatar)));
|
||||
} else if (avatarHandler != null) {
|
||||
if (avatarHandler.canSet()) menu.add(item("Set avatar…", "UPLOAD_AVATAR", avatarHandler::set));
|
||||
menu.add(item("Delete avatar", "DELETE_AVATAR", avatarHandler::delete));
|
||||
}
|
||||
menu.show(pane, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
private static JMenuItem item(String text, String icon, Runnable action) {
|
||||
JMenuItem item = new JMenuItem(text, icon == null ? null : Icons.of(icon));
|
||||
item.addActionListener(e -> action.run());
|
||||
return item;
|
||||
}
|
||||
|
||||
/** Whether {@code point} falls on the avatar's {@code <img>}. */
|
||||
private boolean overAvatar(Avatar avatar, Point point) {
|
||||
if (avatar.file() == null) return false;
|
||||
|
||||
@@ -232,9 +232,24 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
changeNickname();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSetAvatar() {
|
||||
return selected != null && selected.session().canSetAvatar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAvatar() {
|
||||
if (selected != null && selected.session().isConnected()) selected.showAvatarDialog();
|
||||
if (selected != null) selected.avatar().choose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAvatar() {
|
||||
return selected != null && selected.session().hasAvatar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteAvatar() {
|
||||
if (selected != null) selected.avatar().delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -46,8 +46,16 @@ final class MainMenuBar extends JMenuBar {
|
||||
|
||||
void onChangeNickname();
|
||||
|
||||
/** Whether the current server lets us upload an avatar; asked as the Self menu opens. */
|
||||
boolean canSetAvatar();
|
||||
|
||||
void onSetAvatar();
|
||||
|
||||
/** Whether we have an avatar on the current server to delete; asked as the Self menu opens. */
|
||||
boolean hasAvatar();
|
||||
|
||||
void onDeleteAvatar();
|
||||
|
||||
void onShowIdentities();
|
||||
|
||||
void onShowContacts();
|
||||
@@ -77,6 +85,7 @@ final class MainMenuBar extends JMenuBar {
|
||||
private JMenuItem awayStatusItem;
|
||||
private JCheckBoxMenuItem commanderItem;
|
||||
private JMenuItem avatarItem;
|
||||
private JMenuItem deleteAvatarItem;
|
||||
private JMenuItem banListItem;
|
||||
|
||||
MainMenuBar(Bookmarks bookmarks, Listener listener) {
|
||||
@@ -118,6 +127,8 @@ final class MainMenuBar extends JMenuBar {
|
||||
nick.addActionListener(e -> listener.onChangeNickname());
|
||||
avatarItem = new JMenuItem("Set avatar…", Icons.of("UPLOAD_AVATAR"));
|
||||
avatarItem.addActionListener(e -> listener.onSetAvatar());
|
||||
deleteAvatarItem = new JMenuItem("Delete avatar", Icons.of("DELETE_AVATAR"));
|
||||
deleteAvatarItem.addActionListener(e -> listener.onDeleteAvatar());
|
||||
self.add(mute);
|
||||
self.add(deaf);
|
||||
self.addSeparator();
|
||||
@@ -127,6 +138,22 @@ final class MainMenuBar extends JMenuBar {
|
||||
self.addSeparator();
|
||||
self.add(nick);
|
||||
self.add(avatarItem);
|
||||
self.add(deleteAvatarItem);
|
||||
self.addMenuListener(new MenuListener() {
|
||||
@Override
|
||||
public void menuSelected(MenuEvent e) {
|
||||
avatarItem.setVisible(!avatarItem.isEnabled() || listener.canSetAvatar());
|
||||
deleteAvatarItem.setVisible(listener.hasAvatar());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuDeselected(MenuEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuCanceled(MenuEvent e) {
|
||||
}
|
||||
});
|
||||
|
||||
JMenu tools = new JMenu("Tools");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…", Icons.of("IDENTITY_MANAGER"));
|
||||
@@ -181,6 +208,7 @@ final class MainMenuBar extends JMenuBar {
|
||||
awayStatusItem.setEnabled(connected);
|
||||
commanderItem.setEnabled(connected);
|
||||
avatarItem.setEnabled(connected);
|
||||
deleteAvatarItem.setEnabled(connected);
|
||||
banListItem.setEnabled(connected);
|
||||
awayItem.setSelected(away);
|
||||
commanderItem.setSelected(commander);
|
||||
|
||||
@@ -15,15 +15,10 @@ import com.ts3client.sound.SoundNotifier;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
/**
|
||||
* One server connection and the views bound to it (tree, info and chat). The
|
||||
@@ -46,6 +41,7 @@ final class ServerTab {
|
||||
private final ServerTreePanel treePanel;
|
||||
private final ChatPanel chatPanel;
|
||||
private final InfoPanel infoPanel;
|
||||
private final AvatarActions avatar;
|
||||
private final JComponent component;
|
||||
private JSplitPane leftColumn;
|
||||
private int normalDividerSize;
|
||||
@@ -107,7 +103,28 @@ final class ServerTab {
|
||||
}
|
||||
});
|
||||
|
||||
infoPanel.setAvatarStealer(this::stealAvatar);
|
||||
this.avatar = new AvatarActions(host, conn);
|
||||
infoPanel.setAvatarHandler(new InfoPanel.AvatarHandler() {
|
||||
@Override
|
||||
public void steal(Avatar other) {
|
||||
avatar.steal(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSet() {
|
||||
return session.canSetAvatar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set() {
|
||||
avatar.choose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
avatar.delete();
|
||||
}
|
||||
});
|
||||
|
||||
leftColumn = new JSplitPane(JSplitPane.VERTICAL_SPLIT, treePanel, infoPanel);
|
||||
leftColumn.setResizeWeight(0.68);
|
||||
@@ -175,29 +192,8 @@ final class ServerTab {
|
||||
|
||||
// ---- dialogs ----
|
||||
|
||||
void showAvatarDialog() {
|
||||
if (conn.isConnected()) new AvatarDialog(host, conn).setVisible(true);
|
||||
}
|
||||
|
||||
/** Uploads someone else's avatar as ours. */
|
||||
private void stealAvatar(Avatar avatar) {
|
||||
if (!conn.isConnected()) return;
|
||||
File file;
|
||||
try {
|
||||
file = Files.createTempFile("avatar", "." + avatar.format().extension).toFile();
|
||||
Files.write(file.toPath(), avatar.data());
|
||||
} catch (IOException e) {
|
||||
showAvatarError(e.getMessage());
|
||||
return;
|
||||
}
|
||||
conn.setAvatar(file, error -> {
|
||||
file.delete();
|
||||
if (error != null) SwingUtilities.invokeLater(() -> showAvatarError(error));
|
||||
});
|
||||
}
|
||||
|
||||
private void showAvatarError(String error) {
|
||||
JOptionPane.showMessageDialog(host, "Error uploading avatar: " + error, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
AvatarActions avatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
// ---- chat ----
|
||||
|
||||
Reference in New Issue
Block a user