feat(activity): foco de peer, badges contados vs indicador
PeerActivityFocus suprime notificações no canal visível; screencast só prepara IP e signal; chat sem eco na sala. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
f6b2cf3ecd
commit
1414f59a20
16 files changed
+497
-56
No files matched your search
@@ -20,5 +20,6 @@
|
|||||||
| D20 | Peer actions por plugin no popup | Lista lateral agrupa ações de todos os plugins (submenus), não só a aba ativa | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
| D20 | Peer actions por plugin no popup | Lista lateral agrupa ações de todos os plugins (submenus), não só a aba ativa | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
| D21 | `onShellTabClosed` | Fechar aba do plugin na shell notifica o plugin (ex.: parar screencast) | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
| D21 | `onShellTabClosed` | Fechar aba do plugin na shell notifica o plugin (ex.: parar screencast) | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
| D22 | `PeerIpv4` para IP de peer na UI | No Windows, `InetAddress.getHostAddress()` pode devolver IPv6-mapeado e sufixo de zona (`%n`); chat e roteamento usam IPv4 pontilhado | [appendix_portas_e_multicast.md](documentation/appendix_portas_e_multicast.md) |
|
| D22 | `PeerIpv4` para IP de peer na UI | No Windows, `InetAddress.getHostAddress()` pode devolver IPv6-mapeado e sufixo de zona (`%n`); chat e roteamento usam IPv4 pontilhado | [appendix_portas_e_multicast.md](documentation/appendix_portas_e_multicast.md) |
|
||||||
|
| D23 | Foco de atividade e badges | `PeerActivityFocus` + `PeerActivityFocusTokens`; chat usa contagem (`recordCounted`), screencast usa indicador (`signal`); suprime badge no canal em foco | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
|
|
||||||
Novas decisões: adicionar linha nesta tabela e detalhar no arquivo técnico correspondente.
|
Novas decisões: adicionar linha nesta tabela e detalhar no arquivo técnico correspondente.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package pitiupi.api.plugin;
|
||||||
|
|
||||||
|
import pitiupi.presentation.shell.PeerIpv4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokens de foco para suprimir badges enquanto o usuário vê o canal correspondente.
|
||||||
|
*/
|
||||||
|
public final class PeerActivityFocusTokens {
|
||||||
|
|
||||||
|
public static final String BROADCAST = "broadcast";
|
||||||
|
|
||||||
|
private PeerActivityFocusTokens() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String broadcast() {
|
||||||
|
return BROADCAST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String peerInstance(String instanceId) {
|
||||||
|
if (instanceId == null || instanceId.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return "peer:" + instanceId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String peerIp(String ip) {
|
||||||
|
String normalized = PeerIpv4.normalize(ip != null ? ip : "");
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return "peer-ip:" + normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package pitiupi.application.activity;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canal de atividade em foco por plugin (sub-aba / sessão visível).
|
||||||
|
*/
|
||||||
|
public final class PeerActivityFocus {
|
||||||
|
|
||||||
|
private final Map<String, String> focusedChannelByPlugin = new ConcurrentHashMap<>();
|
||||||
|
private PeerActivityInbox inbox;
|
||||||
|
|
||||||
|
public void bindInbox(PeerActivityInbox inbox) {
|
||||||
|
this.inbox = inbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFocusedChannel(String pluginId, String focusToken) {
|
||||||
|
if (pluginId == null || pluginId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (focusToken == null || focusToken.isBlank()) {
|
||||||
|
focusedChannelByPlugin.remove(pluginId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
focusedChannelByPlugin.put(pluginId, focusToken);
|
||||||
|
if (inbox != null) {
|
||||||
|
inbox.clearForPluginFocus(pluginId, focusToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(String pluginId) {
|
||||||
|
if (pluginId != null) {
|
||||||
|
focusedChannelByPlugin.remove(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFocused(String pluginId, String focusToken) {
|
||||||
|
if (pluginId == null || focusToken == null || focusToken.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String current = focusedChannelByPlugin.get(pluginId);
|
||||||
|
return focusToken.equals(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +1,161 @@
|
|||||||
package pitiupi.application.activity;
|
package pitiupi.application.activity;
|
||||||
|
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pendências de atividade por peer (mensagem, tela, etc.) para badges na lista lateral.
|
* Pendências de atividade por peer: contagem (chat) ou indicador (screencast).
|
||||||
*/
|
*/
|
||||||
public final class PeerActivityInbox {
|
public final class PeerActivityInbox {
|
||||||
|
|
||||||
public static final String BROADCAST_PEER_KEY = "__broadcast__";
|
public static final String BROADCAST_PEER_KEY = "__broadcast__";
|
||||||
|
|
||||||
private final Map<String, Map<String, Integer>> countByPeerAndAction = new ConcurrentHashMap<>();
|
private final Map<String, Map<String, Integer>> countedByPeerAndAction = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Map<String, Integer>> indicatorByPeerAndAction = new ConcurrentHashMap<>();
|
||||||
|
private PeerActivityFocus focus;
|
||||||
private Runnable onChanged = () -> {
|
private Runnable onChanged = () -> {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public void setFocus(PeerActivityFocus focus) {
|
||||||
|
this.focus = focus;
|
||||||
|
if (focus != null) {
|
||||||
|
focus.bindInbox(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setOnChanged(Runnable onChanged) {
|
public void setOnChanged(Runnable onChanged) {
|
||||||
this.onChanged = onChanged != null ? onChanged : () -> {
|
this.onChanged = onChanged != null ? onChanged : () -> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated use {@link #recordCounted} */
|
||||||
|
@Deprecated
|
||||||
public void record(String peerInstanceId, String pluginId, String peerActionId) {
|
public void record(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
String peerKey = peerKey(peerInstanceId);
|
recordCounted(peerInstanceId, pluginId, peerActionId, null);
|
||||||
String actionKey = actionKey(pluginId, peerActionId);
|
|
||||||
countByPeerAndAction.computeIfAbsent(peerKey, k -> new ConcurrentHashMap<>())
|
|
||||||
.merge(actionKey, 1, Integer::sum);
|
|
||||||
onChanged.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recordBroadcast(String pluginId, String peerActionId) {
|
public void recordBroadcast(String pluginId, String peerActionId) {
|
||||||
record(BROADCAST_PEER_KEY, pluginId, peerActionId);
|
recordCounted(BROADCAST_PEER_KEY, pluginId, peerActionId, PeerActivityFocusTokens.broadcast());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void recordCounted(String peerInstanceId, String pluginId, String peerActionId, String focusToken) {
|
||||||
|
if (shouldSkip(pluginId, focusToken)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String peerKey = peerKey(peerInstanceId);
|
||||||
|
String actionKey = actionKey(pluginId, peerActionId);
|
||||||
|
countedByPeerAndAction.computeIfAbsent(peerKey, k -> new ConcurrentHashMap<>())
|
||||||
|
.merge(actionKey, 1, Integer::sum);
|
||||||
|
notifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordBroadcastCounted(String pluginId, String peerActionId, String focusToken) {
|
||||||
|
recordCounted(BROADCAST_PEER_KEY, pluginId, peerActionId, focusToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void signal(String peerInstanceId, String pluginId, String peerActionId, String focusToken) {
|
||||||
|
if (shouldSkip(pluginId, focusToken)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String peerKey = peerKey(peerInstanceId);
|
||||||
|
String actionKey = actionKey(pluginId, peerActionId);
|
||||||
|
indicatorByPeerAndAction.computeIfAbsent(peerKey, k -> new ConcurrentHashMap<>())
|
||||||
|
.put(actionKey, 1);
|
||||||
|
notifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void signalBroadcast(String pluginId, String peerActionId, String focusToken) {
|
||||||
|
signal(BROADCAST_PEER_KEY, pluginId, peerActionId, focusToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int totalCountedForPeer(String peerInstanceId) {
|
||||||
|
return sum(countedByPeerAndAction.getOrDefault(peerKey(peerInstanceId), Map.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use {@link #totalCountedForPeer} */
|
||||||
|
@Deprecated
|
||||||
public int totalForPeer(String peerInstanceId) {
|
public int totalForPeer(String peerInstanceId) {
|
||||||
return sum(countByPeerAndAction.getOrDefault(peerKey(peerInstanceId), Map.of()));
|
return totalCountedForPeer(peerInstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasIndicatorForPeer(String peerInstanceId) {
|
||||||
|
Map<String, Integer> map = indicatorByPeerAndAction.get(peerKey(peerInstanceId));
|
||||||
|
return map != null && !map.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int countForAction(String peerInstanceId, String pluginId, String peerActionId) {
|
public int countForAction(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
return countByPeerAndAction
|
return countedByPeerAndAction
|
||||||
.getOrDefault(peerKey(peerInstanceId), Map.of())
|
.getOrDefault(peerKey(peerInstanceId), Map.of())
|
||||||
.getOrDefault(actionKey(pluginId, peerActionId), 0);
|
.getOrDefault(actionKey(pluginId, peerActionId), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasIndicatorForAction(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
|
return indicatorByPeerAndAction
|
||||||
|
.getOrDefault(peerKey(peerInstanceId), Map.of())
|
||||||
|
.containsKey(actionKey(pluginId, peerActionId));
|
||||||
|
}
|
||||||
|
|
||||||
public void clearAction(String peerInstanceId, String pluginId, String peerActionId) {
|
public void clearAction(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
Map<String, Integer> actions = countByPeerAndAction.get(peerKey(peerInstanceId));
|
String peerKey = peerKey(peerInstanceId);
|
||||||
if (actions != null) {
|
String actionKey = actionKey(pluginId, peerActionId);
|
||||||
actions.remove(actionKey(pluginId, peerActionId));
|
removeAction(peerKey, actionKey);
|
||||||
if (actions.isEmpty()) {
|
notifyChanged();
|
||||||
countByPeerAndAction.remove(peerKey(peerInstanceId));
|
}
|
||||||
|
|
||||||
|
void clearForPluginFocus(String pluginId, String focusToken) {
|
||||||
|
String peerKey = peerKeyFromFocusToken(focusToken);
|
||||||
|
if (peerKey == null || pluginId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String prefix = pluginId + ":";
|
||||||
|
clearPluginActionsOnPeer(peerKey, prefix);
|
||||||
|
notifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearPluginActionsOnPeer(String peerKey, String pluginIdPrefix) {
|
||||||
|
Map<String, Integer> counted = countedByPeerAndAction.get(peerKey);
|
||||||
|
if (counted != null) {
|
||||||
|
counted.keySet().removeIf(k -> k.startsWith(pluginIdPrefix));
|
||||||
|
if (counted.isEmpty()) {
|
||||||
|
countedByPeerAndAction.remove(peerKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Map<String, Integer> indicators = indicatorByPeerAndAction.get(peerKey);
|
||||||
|
if (indicators != null) {
|
||||||
|
indicators.keySet().removeIf(k -> k.startsWith(pluginIdPrefix));
|
||||||
|
if (indicators.isEmpty()) {
|
||||||
|
indicatorByPeerAndAction.remove(peerKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAction(String peerKey, String actionKey) {
|
||||||
|
Map<String, Integer> counted = countedByPeerAndAction.get(peerKey);
|
||||||
|
if (counted != null) {
|
||||||
|
counted.remove(actionKey);
|
||||||
|
if (counted.isEmpty()) {
|
||||||
|
countedByPeerAndAction.remove(peerKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Integer> indicators = indicatorByPeerAndAction.get(peerKey);
|
||||||
|
if (indicators != null) {
|
||||||
|
indicators.remove(actionKey);
|
||||||
|
if (indicators.isEmpty()) {
|
||||||
|
indicatorByPeerAndAction.remove(peerKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldSkip(String pluginId, String focusToken) {
|
||||||
|
return focus != null
|
||||||
|
&& focusToken != null
|
||||||
|
&& !focusToken.isBlank()
|
||||||
|
&& focus.isFocused(pluginId, focusToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyChanged() {
|
||||||
onChanged.run();
|
onChanged.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +167,26 @@ public final class PeerActivityInbox {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String peerKeyFromFocusToken(String focusToken) {
|
||||||
|
if (focusToken == null || focusToken.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (PeerActivityFocusTokens.BROADCAST.equals(focusToken)) {
|
||||||
|
return BROADCAST_PEER_KEY;
|
||||||
|
}
|
||||||
|
if (focusToken.startsWith("peer:")) {
|
||||||
|
return focusToken.substring("peer:".length());
|
||||||
|
}
|
||||||
|
if (focusToken.startsWith("peer-ip:")) {
|
||||||
|
return BROADCAST_PEER_KEY;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String peerKey(String peerInstanceId) {
|
private static String peerKey(String peerInstanceId) {
|
||||||
|
if (BROADCAST_PEER_KEY.equals(peerInstanceId)) {
|
||||||
|
return BROADCAST_PEER_KEY;
|
||||||
|
}
|
||||||
return peerInstanceId != null ? peerInstanceId : BROADCAST_PEER_KEY;
|
return peerInstanceId != null ? peerInstanceId : BROADCAST_PEER_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ public final class PresenceService {
|
|||||||
|
|
||||||
public List<UserPeerView> listPeerViews() {
|
public List<UserPeerView> listPeerViews() {
|
||||||
List<UserPeerView> views = new ArrayList<>();
|
List<UserPeerView> views = new ArrayList<>();
|
||||||
int broadcastActivity = peerActivityInbox != null
|
int broadcastCounted = peerActivityInbox != null
|
||||||
? peerActivityInbox.totalForPeer(PeerActivityInbox.BROADCAST_PEER_KEY)
|
? peerActivityInbox.totalCountedForPeer(PeerActivityInbox.BROADCAST_PEER_KEY)
|
||||||
: 0;
|
: 0;
|
||||||
views.add(new UserPeerView("Todos", null, null, broadcastActivity, false, true));
|
boolean broadcastIndicator = peerActivityInbox != null
|
||||||
|
&& peerActivityInbox.hasIndicatorForPeer(PeerActivityInbox.BROADCAST_PEER_KEY);
|
||||||
|
views.add(new UserPeerView("Todos", null, null, broadcastCounted, broadcastIndicator, false, true));
|
||||||
String selfId = heartbeatManager.getInstanceId();
|
String selfId = heartbeatManager.getInstanceId();
|
||||||
for (User user : heartbeatManager.getPeers()) {
|
for (User user : heartbeatManager.getPeers()) {
|
||||||
boolean isSelf = selfId.equals(user.getInstanceId());
|
boolean isSelf = selfId.equals(user.getInstanceId());
|
||||||
@@ -59,14 +61,17 @@ public final class PresenceService {
|
|||||||
if (inviteInboxService != null && user.getInstanceId() != null) {
|
if (inviteInboxService != null && user.getInstanceId() != null) {
|
||||||
notifications += inviteInboxService.getNotificationCount(user.getInstanceId());
|
notifications += inviteInboxService.getNotificationCount(user.getInstanceId());
|
||||||
}
|
}
|
||||||
|
boolean activityIndicator = false;
|
||||||
if (peerActivityInbox != null && user.getInstanceId() != null) {
|
if (peerActivityInbox != null && user.getInstanceId() != null) {
|
||||||
notifications += peerActivityInbox.totalForPeer(user.getInstanceId());
|
notifications += peerActivityInbox.totalCountedForPeer(user.getInstanceId());
|
||||||
|
activityIndicator = peerActivityInbox.hasIndicatorForPeer(user.getInstanceId());
|
||||||
}
|
}
|
||||||
views.add(new UserPeerView(
|
views.add(new UserPeerView(
|
||||||
user.getUserName(),
|
user.getUserName(),
|
||||||
user.getInstanceId(),
|
user.getInstanceId(),
|
||||||
user.getAddress(),
|
user.getAddress(),
|
||||||
notifications,
|
notifications,
|
||||||
|
activityIndicator,
|
||||||
isSelf,
|
isSelf,
|
||||||
false
|
false
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public final class UserPeerView {
|
|||||||
private final String instanceId;
|
private final String instanceId;
|
||||||
private final InetAddress inetAddress;
|
private final InetAddress inetAddress;
|
||||||
private final int notificationCount;
|
private final int notificationCount;
|
||||||
|
private final boolean activityIndicator;
|
||||||
private final boolean isSelf;
|
private final boolean isSelf;
|
||||||
private final boolean isBroadcastTarget;
|
private final boolean isBroadcastTarget;
|
||||||
|
|
||||||
@@ -22,11 +23,24 @@ public final class UserPeerView {
|
|||||||
int notificationCount,
|
int notificationCount,
|
||||||
boolean isSelf,
|
boolean isSelf,
|
||||||
boolean isBroadcastTarget
|
boolean isBroadcastTarget
|
||||||
|
) {
|
||||||
|
this(displayName, instanceId, inetAddress, notificationCount, false, isSelf, isBroadcastTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPeerView(
|
||||||
|
String displayName,
|
||||||
|
String instanceId,
|
||||||
|
InetAddress inetAddress,
|
||||||
|
int notificationCount,
|
||||||
|
boolean activityIndicator,
|
||||||
|
boolean isSelf,
|
||||||
|
boolean isBroadcastTarget
|
||||||
) {
|
) {
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
this.instanceId = instanceId;
|
this.instanceId = instanceId;
|
||||||
this.inetAddress = inetAddress;
|
this.inetAddress = inetAddress;
|
||||||
this.notificationCount = notificationCount;
|
this.notificationCount = notificationCount;
|
||||||
|
this.activityIndicator = activityIndicator;
|
||||||
this.isSelf = isSelf;
|
this.isSelf = isSelf;
|
||||||
this.isBroadcastTarget = isBroadcastTarget;
|
this.isBroadcastTarget = isBroadcastTarget;
|
||||||
}
|
}
|
||||||
@@ -47,6 +61,10 @@ public final class UserPeerView {
|
|||||||
return notificationCount;
|
return notificationCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasActivityIndicator() {
|
||||||
|
return activityIndicator;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSelf() {
|
public boolean isSelf() {
|
||||||
return isSelf;
|
return isSelf;
|
||||||
}
|
}
|
||||||
@@ -56,6 +74,6 @@ public final class UserPeerView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static UserPeerView broadcastTarget() {
|
public static UserPeerView broadcastTarget() {
|
||||||
return new UserPeerView("Todos", null, null, 0, false, true);
|
return new UserPeerView("Todos", null, null, 0, false, false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package pitiupi.control;
|
package pitiupi.control;
|
||||||
|
|
||||||
|
import pitiupi.application.activity.PeerActivityFocus;
|
||||||
import pitiupi.application.activity.PeerActivityInbox;
|
import pitiupi.application.activity.PeerActivityInbox;
|
||||||
import pitiupi.application.invites.InviteInboxService;
|
import pitiupi.application.invites.InviteInboxService;
|
||||||
import pitiupi.application.peeractions.PeerActionBroker;
|
import pitiupi.application.peeractions.PeerActionBroker;
|
||||||
@@ -26,6 +27,7 @@ public class ConnectionManager {
|
|||||||
private final PeerActionBroker peerActionBroker;
|
private final PeerActionBroker peerActionBroker;
|
||||||
private final InviteInboxService inviteInboxService;
|
private final InviteInboxService inviteInboxService;
|
||||||
private final PeerActivityInbox peerActivityInbox;
|
private final PeerActivityInbox peerActivityInbox;
|
||||||
|
private final PeerActivityFocus peerActivityFocus;
|
||||||
|
|
||||||
private SocketTCP socketTCP;
|
private SocketTCP socketTCP;
|
||||||
private SocketUDP socketUDP;
|
private SocketUDP socketUDP;
|
||||||
@@ -49,6 +51,8 @@ public class ConnectionManager {
|
|||||||
peerActionBroker = new PeerActionBroker();
|
peerActionBroker = new PeerActionBroker();
|
||||||
inviteInboxService = new InviteInboxService(heartbeatManager);
|
inviteInboxService = new InviteInboxService(heartbeatManager);
|
||||||
peerActivityInbox = new PeerActivityInbox();
|
peerActivityInbox = new PeerActivityInbox();
|
||||||
|
peerActivityFocus = new PeerActivityFocus();
|
||||||
|
peerActivityInbox.setFocus(peerActivityFocus);
|
||||||
presenceService = new PresenceService(heartbeatManager, inviteInboxService, peerActivityInbox);
|
presenceService = new PresenceService(heartbeatManager, inviteInboxService, peerActivityInbox);
|
||||||
inviteInboxService.setOnNotificationsChanged(heartbeatManager::notifyPeerListeners);
|
inviteInboxService.setOnNotificationsChanged(heartbeatManager::notifyPeerListeners);
|
||||||
peerActivityInbox.setOnChanged(heartbeatManager::notifyPeerListeners);
|
peerActivityInbox.setOnChanged(heartbeatManager::notifyPeerListeners);
|
||||||
@@ -73,6 +77,10 @@ public class ConnectionManager {
|
|||||||
return peerActivityInbox;
|
return peerActivityInbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PeerActivityFocus getPeerActivityFocus() {
|
||||||
|
return peerActivityFocus;
|
||||||
|
}
|
||||||
|
|
||||||
public void setInviteAcceptedUiListener(Consumer<String> listener) {
|
public void setInviteAcceptedUiListener(Consumer<String> listener) {
|
||||||
this.inviteAcceptedUiListener = listener != null ? listener : pluginId -> {
|
this.inviteAcceptedUiListener = listener != null ? listener : pluginId -> {
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ public final class MainShellFrame extends JFrame implements ShellHost {
|
|||||||
if (activeId != null) {
|
if (activeId != null) {
|
||||||
connectionManager.getPeerActionBroker().setActivePlugin(activeId);
|
connectionManager.getPeerActionBroker().setActivePlugin(activeId);
|
||||||
}
|
}
|
||||||
|
for (Plugin plugin : pluginManager.getLoadedPlugins()) {
|
||||||
|
String pluginId = plugin.getIdentifier();
|
||||||
|
if (activeId == null || !activeId.equals(pluginId)) {
|
||||||
|
connectionManager.getPeerActivityFocus().clear(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
tabWorkspace.setOnSettingsTabSelected(settingsPanel::onTabSelected);
|
tabWorkspace.setOnSettingsTabSelected(settingsPanel::onTabSelected);
|
||||||
tabWorkspace.setOnPluginTabClosed(pluginManager::notifyShellTabClosed);
|
tabWorkspace.setOnPluginTabClosed(pluginManager::notifyShellTabClosed);
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
|||||||
if (value instanceof DisplayPeer peer) {
|
if (value instanceof DisplayPeer peer) {
|
||||||
label.setText(ShellUi.formatPeerListLabel(
|
label.setText(ShellUi.formatPeerListLabel(
|
||||||
peer.view.getDisplayName(),
|
peer.view.getDisplayName(),
|
||||||
peer.view.getNotificationCount()
|
peer.view.getNotificationCount(),
|
||||||
|
peer.view.hasActivityIndicator()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return label;
|
return label;
|
||||||
@@ -101,6 +102,10 @@ public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
|||||||
connectionManager.getPeerActivityInbox().setOnChanged(this::refreshFromPresence);
|
connectionManager.getPeerActivityInbox().setOnChanged(this::refreshFromPresence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isIndicatorAction(String actionId) {
|
||||||
|
return actionId != null && actionId.startsWith("screencast.");
|
||||||
|
}
|
||||||
|
|
||||||
private void showPeerActions(DisplayPeer peer, PeerActionTarget target, int x, int y) {
|
private void showPeerActions(DisplayPeer peer, PeerActionTarget target, int x, int y) {
|
||||||
JPopupMenu popup = new JPopupMenu();
|
JPopupMenu popup = new JPopupMenu();
|
||||||
PeerActionBroker broker = connectionManager.getPeerActionBroker();
|
PeerActionBroker broker = connectionManager.getPeerActionBroker();
|
||||||
@@ -119,8 +124,11 @@ public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
|||||||
int pending = activityPeerKey != null
|
int pending = activityPeerKey != null
|
||||||
? activity.countForAction(activityPeerKey, group.pluginId(), action.getId())
|
? activity.countForAction(activityPeerKey, group.pluginId(), action.getId())
|
||||||
: 0;
|
: 0;
|
||||||
|
boolean indicator = activityPeerKey != null
|
||||||
|
&& activity.hasIndicatorForAction(activityPeerKey, group.pluginId(), action.getId());
|
||||||
|
boolean indicatorOnly = indicator && pending == 0 && isIndicatorAction(action.getId());
|
||||||
JMenuItem item = new JMenuItem(
|
JMenuItem item = new JMenuItem(
|
||||||
ShellUi.formatActionLabelWithBadge(action.getLabel(), pending)
|
ShellUi.formatActionLabelWithBadge(action.getLabel(), pending, indicatorOnly)
|
||||||
);
|
);
|
||||||
item.addActionListener(e -> {
|
item.addActionListener(e -> {
|
||||||
if (activityPeerKey != null) {
|
if (activityPeerKey != null) {
|
||||||
|
|||||||
@@ -130,13 +130,35 @@ public final class ShellUi {
|
|||||||
scroll.setViewportBorder(BorderFactory.createEmptyBorder());
|
scroll.setViewportBorder(BorderFactory.createEmptyBorder());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String formatPeerListLabel(String displayName, int notificationCount) {
|
public static String formatPeerListLabel(String displayName, int notificationCount, boolean activityIndicator) {
|
||||||
if (notificationCount <= 0) {
|
if (notificationCount <= 0 && !activityIndicator) {
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
return "<html>" + notificationBadgePrefix()
|
StringBuilder html = new StringBuilder("<html>");
|
||||||
+ escapeHtml(displayName)
|
if (activityIndicator || notificationCount > 0) {
|
||||||
+ " <span style='color:" + NOTIFICATION_COUNT_COLOR + "'>(" + notificationCount + ")</span></html>";
|
html.append(notificationBadgePrefix());
|
||||||
|
}
|
||||||
|
html.append(escapeHtml(displayName));
|
||||||
|
if (notificationCount > 0) {
|
||||||
|
html.append(" <span style='color:").append(NOTIFICATION_COUNT_COLOR).append("'>(")
|
||||||
|
.append(notificationCount).append(")</span>");
|
||||||
|
}
|
||||||
|
html.append("</html>");
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatPeerListLabel(String displayName, int notificationCount) {
|
||||||
|
return formatPeerListLabel(displayName, notificationCount, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatActionLabelWithBadge(String label, int pendingCount, boolean indicatorOnly) {
|
||||||
|
if (pendingCount <= 0 && !indicatorOnly) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
if (indicatorOnly) {
|
||||||
|
return "<html>" + notificationBadgePrefix() + escapeHtml(label) + "</html>";
|
||||||
|
}
|
||||||
|
return formatActionLabelWithBadge(label, pendingCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String formatActionLabelWithBadge(String label, int pendingCount) {
|
public static String formatActionLabelWithBadge(String label, int pendingCount) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package pitiupi.application.activity;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class PeerActivityFocusTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void focusClearsPendingForPluginChannel() {
|
||||||
|
PeerActivityInbox inbox = new PeerActivityInbox();
|
||||||
|
PeerActivityFocus focus = new PeerActivityFocus();
|
||||||
|
inbox.setFocus(focus);
|
||||||
|
|
||||||
|
inbox.recordCounted("peer-1", "plugin.chat", "chat.peer", PeerActivityFocusTokens.peerInstance("peer-1"));
|
||||||
|
assertEquals(1, inbox.totalCountedForPeer("peer-1"));
|
||||||
|
|
||||||
|
focus.setFocusedChannel("plugin.chat", PeerActivityFocusTokens.peerInstance("peer-1"));
|
||||||
|
assertEquals(0, inbox.totalCountedForPeer("peer-1"));
|
||||||
|
assertTrue(focus.isFocused("plugin.chat", PeerActivityFocusTokens.peerInstance("peer-1")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordSkippedWhileFocused() {
|
||||||
|
PeerActivityInbox inbox = new PeerActivityInbox();
|
||||||
|
PeerActivityFocus focus = new PeerActivityFocus();
|
||||||
|
inbox.setFocus(focus);
|
||||||
|
focus.setFocusedChannel("plugin.chat", PeerActivityFocusTokens.broadcast());
|
||||||
|
|
||||||
|
inbox.recordBroadcastCounted("plugin.chat", "chat.focus", PeerActivityFocusTokens.broadcast());
|
||||||
|
assertEquals(0, inbox.totalCountedForPeer(PeerActivityInbox.BROADCAST_PEER_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signalCapsAtOne() {
|
||||||
|
PeerActivityInbox inbox = new PeerActivityInbox();
|
||||||
|
inbox.signalBroadcast("plugin.cast", "screencast.multicast", PeerActivityFocusTokens.broadcast());
|
||||||
|
inbox.signalBroadcast("plugin.cast", "screencast.multicast", PeerActivityFocusTokens.broadcast());
|
||||||
|
assertTrue(inbox.hasIndicatorForPeer(PeerActivityInbox.BROADCAST_PEER_KEY));
|
||||||
|
assertTrue(inbox.hasIndicatorForAction(
|
||||||
|
PeerActivityInbox.BROADCAST_PEER_KEY, "plugin.cast", "screencast.multicast"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
package pitiupi.application.activity;
|
package pitiupi.application.activity;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
class PeerActivityInboxTest {
|
class PeerActivityInboxTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void recordsAndClearsPerAction() {
|
void recordsAndClearsCountedPerAction() {
|
||||||
PeerActivityInbox inbox = new PeerActivityInbox();
|
PeerActivityInbox inbox = new PeerActivityInbox();
|
||||||
inbox.record("peer-1", "plugin.chat", "chat.peer");
|
inbox.recordCounted("peer-1", "plugin.chat", "chat.peer", null);
|
||||||
inbox.record("peer-1", "plugin.chat", "chat.peer");
|
inbox.recordCounted("peer-1", "plugin.chat", "chat.peer", null);
|
||||||
assertEquals(2, inbox.countForAction("peer-1", "plugin.chat", "chat.peer"));
|
assertEquals(2, inbox.countForAction("peer-1", "plugin.chat", "chat.peer"));
|
||||||
assertEquals(2, inbox.totalForPeer("peer-1"));
|
assertEquals(2, inbox.totalCountedForPeer("peer-1"));
|
||||||
inbox.clearAction("peer-1", "plugin.chat", "chat.peer");
|
inbox.clearAction("peer-1", "plugin.chat", "chat.peer");
|
||||||
assertEquals(0, inbox.totalForPeer("peer-1"));
|
assertEquals(0, inbox.totalCountedForPeer("peer-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signalIsIndicatorNotCounted() {
|
||||||
|
PeerActivityInbox inbox = new PeerActivityInbox();
|
||||||
|
inbox.signal("peer-1", "plugin.cast", "screencast.peer", null);
|
||||||
|
inbox.signal("peer-1", "plugin.cast", "screencast.peer", null);
|
||||||
|
assertEquals(0, inbox.totalCountedForPeer("peer-1"));
|
||||||
|
assertTrue(inbox.hasIndicatorForPeer("peer-1"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import pitiupi.application.messaging.MessageDispatcher;
|
|||||||
import pitiupi.application.messaging.PluginTransportCatalog;
|
import pitiupi.application.messaging.PluginTransportCatalog;
|
||||||
import pitiupi.application.messaging.SendTarget;
|
import pitiupi.application.messaging.SendTarget;
|
||||||
import pitiupi.application.messaging.TransportMode;
|
import pitiupi.application.messaging.TransportMode;
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
import pitiupi.application.presence.UserPeerView;
|
import pitiupi.application.presence.UserPeerView;
|
||||||
import pitiupi.chat.control.PrivateChatController;
|
import pitiupi.chat.control.PrivateChatController;
|
||||||
import pitiupi.chat.control.PrivateChatUiSink;
|
import pitiupi.chat.control.PrivateChatUiSink;
|
||||||
@@ -68,6 +69,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
|||||||
private PrivateChatController privateChatController;
|
private PrivateChatController privateChatController;
|
||||||
private PeerDraftPanel draftPanel;
|
private PeerDraftPanel draftPanel;
|
||||||
private Runnable pendingTranscriptFlush;
|
private Runnable pendingTranscriptFlush;
|
||||||
|
private String pluginId;
|
||||||
|
|
||||||
public ChatPanel(ConnectionManager connectionManager) {
|
public ChatPanel(ConnectionManager connectionManager) {
|
||||||
this.connectionManager = connectionManager;
|
this.connectionManager = connectionManager;
|
||||||
@@ -100,6 +102,10 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
|||||||
selectRoomSession();
|
selectRoomSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPluginId(String pluginId) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
private void initSessionTabs() {
|
private void initSessionTabs() {
|
||||||
sessionTabs.addOrSelectTab(
|
sessionTabs.addOrSelectTab(
|
||||||
PluginSessionKeys.ROOM,
|
PluginSessionKeys.ROOM,
|
||||||
@@ -146,6 +152,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
|||||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||||
routingPanel.bindRoomSession();
|
routingPanel.bindRoomSession();
|
||||||
switchActiveConversation(ROOM_KEY);
|
switchActiveConversation(ROOM_KEY);
|
||||||
|
reportActivityFocusForRoom();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
if (key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||||
@@ -153,9 +160,40 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
|||||||
String name = peerDisplayNames.getOrDefault(ip, ip);
|
String name = peerDisplayNames.getOrDefault(ip, ip);
|
||||||
routingPanel.bindPeerSession(ip, name);
|
routingPanel.bindPeerSession(ip, name);
|
||||||
switchActiveConversation(resolveConversationKey());
|
switchActiveConversation(resolveConversationKey());
|
||||||
|
reportActivityFocusForPeerIp(ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reportActivityFocusForRoom() {
|
||||||
|
if (pluginId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectionManager.getPeerActivityFocus()
|
||||||
|
.setFocusedChannel(pluginId, PeerActivityFocusTokens.broadcast());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportActivityFocusForPeerIp(String ip) {
|
||||||
|
if (pluginId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectionManager.getPeerActivityFocus()
|
||||||
|
.setFocusedChannel(pluginId, resolvePeerFocusToken(ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePeerFocusToken(String ip) {
|
||||||
|
String canonical = canonicalPeerIp(ip);
|
||||||
|
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||||
|
if (view.isBroadcastTarget() || view.isSelf()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (view.getInstanceId() != null
|
||||||
|
&& view.getInetAddress().map(PeerIpv4::fromInetAddress).orElse("").equals(canonical)) {
|
||||||
|
return PeerActivityFocusTokens.peerInstance(view.getInstanceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PeerActivityFocusTokens.peerIp(canonical);
|
||||||
|
}
|
||||||
|
|
||||||
private void openDraftPeerTab() {
|
private void openDraftPeerTab() {
|
||||||
String previous = sessionTabs.selectedKey();
|
String previous = sessionTabs.selectedKey();
|
||||||
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
|
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
|
||||||
@@ -260,6 +298,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void focusGeneralRoom() {
|
public void focusGeneralRoom() {
|
||||||
|
reportActivityFocusForRoom();
|
||||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +459,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
|||||||
appendSystemLine(activeConversationKey, "Informe um IP válido para enviar a um peer.");
|
appendSystemLine(activeConversationKey, "Informe um IP válido para enviar a um peer.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String ip = LocalNetworkInfo.bestLocalIpv4();
|
String ip = PeerIpv4.normalize(LocalNetworkInfo.bestLocalIpv4());
|
||||||
ChatMessage message = new ChatMessage(
|
ChatMessage message = new ChatMessage(
|
||||||
connectionManager.getUserName(),
|
connectionManager.getUserName(),
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package pitiupi.chat.plugin;
|
|||||||
|
|
||||||
import pitiupi.api.plugin.PeerActionsRegistrar;
|
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||||
import pitiupi.api.plugin.PluginWorkspace;
|
import pitiupi.api.plugin.PluginWorkspace;
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
import pitiupi.application.activity.PeerActivityInbox;
|
import pitiupi.application.activity.PeerActivityInbox;
|
||||||
import pitiupi.application.peeractions.PeerAction;
|
import pitiupi.application.peeractions.PeerAction;
|
||||||
import pitiupi.application.peeractions.PeerActionTarget;
|
import pitiupi.application.peeractions.PeerActionTarget;
|
||||||
@@ -41,6 +42,7 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
|
|||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
this.privateChatController = new PrivateChatController(connectionManager, this);
|
this.privateChatController = new PrivateChatController(connectionManager, this);
|
||||||
this.chatPanel = new ChatPanel(connectionManager);
|
this.chatPanel = new ChatPanel(connectionManager);
|
||||||
|
chatPanel.setPluginId(getIdentifier());
|
||||||
chatPanel.setPrivateChatController(privateChatController);
|
chatPanel.setPrivateChatController(privateChatController);
|
||||||
privateChatController.setUiSink(chatPanel);
|
privateChatController.setUiSink(chatPanel);
|
||||||
bindShellWorkspaceContent(chatPanel);
|
bindShellWorkspaceContent(chatPanel);
|
||||||
@@ -119,6 +121,10 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void recordChatActivity(ChatMessage msg) {
|
private void recordChatActivity(ChatMessage msg) {
|
||||||
|
String localName = connectionManager.getUserName();
|
||||||
|
if (localName != null && msg.getName() != null && localName.equals(msg.getName().trim())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (LocalUserIdentity.isLocalSender(connectionManager, msg.getName(), msg.getSenderIp())) {
|
if (LocalUserIdentity.isLocalSender(connectionManager, msg.getName(), msg.getSenderIp())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,12 +132,17 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
|
|||||||
String tag = msg.getTransportLabel();
|
String tag = msg.getTransportLabel();
|
||||||
boolean multicast = tag != null && tag.toLowerCase().contains("multicast");
|
boolean multicast = tag != null && tag.toLowerCase().contains("multicast");
|
||||||
if (multicast) {
|
if (multicast) {
|
||||||
inbox.recordBroadcast(getIdentifier(), "chat.focus");
|
inbox.recordBroadcastCounted(getIdentifier(), "chat.focus", PeerActivityFocusTokens.broadcast());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String instanceId = peerInstanceIdForActivity(msg.getSenderIp());
|
String instanceId = peerInstanceIdForActivity(msg.getSenderIp());
|
||||||
if (instanceId != null) {
|
if (instanceId != null) {
|
||||||
inbox.record(instanceId, getIdentifier(), "chat.peer");
|
inbox.recordCounted(
|
||||||
|
instanceId,
|
||||||
|
getIdentifier(),
|
||||||
|
"chat.peer",
|
||||||
|
PeerActivityFocusTokens.peerInstance(instanceId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +180,12 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
|
|||||||
privateChatController.receiveMessage(connection, legacy);
|
privateChatController.receiveMessage(connection, legacy);
|
||||||
String instanceId = peerInstanceIdForActivity(PeerIpv4.fromInetAddress(connection.getAddress()));
|
String instanceId = peerInstanceIdForActivity(PeerIpv4.fromInetAddress(connection.getAddress()));
|
||||||
if (instanceId != null) {
|
if (instanceId != null) {
|
||||||
connectionManager.getPeerActivityInbox().record(instanceId, getIdentifier(), "chat.private.tcp");
|
connectionManager.getPeerActivityInbox().recordCounted(
|
||||||
|
instanceId,
|
||||||
|
getIdentifier(),
|
||||||
|
"chat.private.tcp",
|
||||||
|
PeerActivityFocusTokens.peerInstance(instanceId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pitiupi.application.messaging.MessageDispatcher;
|
|||||||
import pitiupi.application.messaging.PluginTransportCatalog;
|
import pitiupi.application.messaging.PluginTransportCatalog;
|
||||||
import pitiupi.application.messaging.SendTarget;
|
import pitiupi.application.messaging.SendTarget;
|
||||||
import pitiupi.application.messaging.TransportMode;
|
import pitiupi.application.messaging.TransportMode;
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
import pitiupi.application.presence.UserPeerView;
|
import pitiupi.application.presence.UserPeerView;
|
||||||
import pitiupi.control.ConnectionManager;
|
import pitiupi.control.ConnectionManager;
|
||||||
import pitiupi.presentation.shell.PeerDraftPanel;
|
import pitiupi.presentation.shell.PeerDraftPanel;
|
||||||
@@ -77,6 +78,7 @@ public class ScreenCastPanel extends JPanel {
|
|||||||
private final JPanel bodyCards = new JPanel(bodyLayout);
|
private final JPanel bodyCards = new JPanel(bodyLayout);
|
||||||
private final Map<String, JPanel> bodyPanels = new HashMap<>();
|
private final Map<String, JPanel> bodyPanels = new HashMap<>();
|
||||||
private PeerDraftPanel draftPanel;
|
private PeerDraftPanel draftPanel;
|
||||||
|
private String pluginId;
|
||||||
|
|
||||||
public ScreenCastPanel(ConnectionManager connectionManager) {
|
public ScreenCastPanel(ConnectionManager connectionManager) {
|
||||||
this.connectionManager = connectionManager;
|
this.connectionManager = connectionManager;
|
||||||
@@ -147,14 +149,31 @@ public class ScreenCastPanel extends JPanel {
|
|||||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPluginId(String pluginId) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
private void onSessionTabClosed(String key) {
|
private void onSessionTabClosed(String key) {
|
||||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||||
if (draftPanel != null) {
|
if (draftPanel != null) {
|
||||||
draftPanel.clearIpField();
|
draftPanel.clearIpField();
|
||||||
}
|
}
|
||||||
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
|
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (key != null && key.startsWith("view:")) {
|
||||||
|
peerIpsByViewKey.remove(key);
|
||||||
|
receiveScopes.remove(key);
|
||||||
|
bodyPanels.remove(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean wasSharing = isSharingForTab(key);
|
||||||
sharingByTabKey.remove(key);
|
sharingByTabKey.remove(key);
|
||||||
|
if (wasSharing && key != null && key.equals(shareContextTabKey)) {
|
||||||
|
cancelShareTimer();
|
||||||
|
setShareToggleSelectedSilently(false);
|
||||||
|
applySharingForActiveTab();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restoreSessionAfterDraftClosed() {
|
private void restoreSessionAfterDraftClosed() {
|
||||||
@@ -190,6 +209,48 @@ public class ScreenCastPanel extends JPanel {
|
|||||||
if (!isSharingForTab(key)) {
|
if (!isSharingForTab(key)) {
|
||||||
refreshOutboundBodyForTab(key);
|
refreshOutboundBodyForTab(key);
|
||||||
}
|
}
|
||||||
|
reportActivityFocusForSession(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportActivityFocusForSession(String key) {
|
||||||
|
if (pluginId == null || key == null || PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var focus = connectionManager.getPeerActivityFocus();
|
||||||
|
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||||
|
focus.setFocusedChannel(pluginId, PeerActivityFocusTokens.broadcast());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.startsWith("peer:")) {
|
||||||
|
String ip = key.substring("peer:".length());
|
||||||
|
focus.setFocusedChannel(pluginId, resolvePeerFocusToken(ip));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.startsWith("view:")) {
|
||||||
|
ScreenCastReceiveScope scope = receiveScopes.getOrDefault(key, ScreenCastReceiveScope.BROADCAST);
|
||||||
|
if (scope == ScreenCastReceiveScope.BROADCAST) {
|
||||||
|
focus.setFocusedChannel(pluginId, PeerActivityFocusTokens.broadcast());
|
||||||
|
} else {
|
||||||
|
String ip = peerIpsByViewKey.get(key);
|
||||||
|
if (ip != null && !ip.isBlank()) {
|
||||||
|
focus.setFocusedChannel(pluginId, resolvePeerFocusToken(ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePeerFocusToken(String ip) {
|
||||||
|
String canonical = canonicalPeerIp(ip);
|
||||||
|
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||||
|
if (view.isBroadcastTarget() || view.isSelf()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (view.getInstanceId() != null
|
||||||
|
&& view.getInetAddress().map(PeerIpv4::fromInetAddress).orElse("").equals(canonical)) {
|
||||||
|
return PeerActivityFocusTokens.peerInstance(view.getInstanceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PeerActivityFocusTokens.peerIp(canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistShareStateForContextTab() {
|
private void persistShareStateForContextTab() {
|
||||||
@@ -354,29 +415,18 @@ public class ScreenCastPanel extends JPanel {
|
|||||||
return PeerIpv4.normalize(ip);
|
return PeerIpv4.normalize(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void focusMulticastSharing() {
|
public void focusMulticastSession() {
|
||||||
ensureTodosTab();
|
ensureTodosTab();
|
||||||
sharingByTabKey.put(PluginSessionKeys.ROOM, true);
|
sharingByTabKey.put(PluginSessionKeys.ROOM, false);
|
||||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||||
routingPanel.bindRoomSession();
|
routingPanel.bindRoomSession();
|
||||||
routingPanel.selectTransport(TransportMode.MULTICAST_UDP);
|
routingPanel.selectTransport(TransportMode.MULTICAST_UDP);
|
||||||
setShareToggleSelectedSilently(true);
|
setShareToggleSelectedSilently(false);
|
||||||
applySharingForActiveTab();
|
applySharingForActiveTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void focusPeerSharing(UserPeerView peer) {
|
public void focusPeerSharing(UserPeerView peer) {
|
||||||
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
|
focusPeerAddress(peer);
|
||||||
return;
|
|
||||||
}
|
|
||||||
peer.getInetAddress().ifPresent(addr -> {
|
|
||||||
String ip = PeerIpv4.fromInetAddress(addr);
|
|
||||||
openPeerTab(ip, peer.getDisplayName());
|
|
||||||
String key = PluginSessionKeys.peer(canonicalPeerIp(ip));
|
|
||||||
sharingByTabKey.put(key, true);
|
|
||||||
routingPanel.selectTransport(TransportMode.UNICAST_UDP);
|
|
||||||
setShareToggleSelectedSilently(true);
|
|
||||||
applySharingForActiveTab();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void focusPeerAddress(UserPeerView peer) {
|
public void focusPeerAddress(UserPeerView peer) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import pitiupi.api.plugin.PeerActionsRegistrar;
|
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||||
import pitiupi.api.plugin.PluginWorkspace;
|
import pitiupi.api.plugin.PluginWorkspace;
|
||||||
|
import pitiupi.api.plugin.PeerActivityFocusTokens;
|
||||||
import pitiupi.application.activity.PeerActivityInbox;
|
import pitiupi.application.activity.PeerActivityInbox;
|
||||||
import pitiupi.application.peeractions.PeerAction;
|
import pitiupi.application.peeractions.PeerAction;
|
||||||
import pitiupi.application.presence.UserPeerView;
|
import pitiupi.application.presence.UserPeerView;
|
||||||
@@ -55,6 +56,7 @@ public class ScreenCastPlugin extends Plugin {
|
|||||||
this.connectionManager = connectionManager;
|
this.connectionManager = connectionManager;
|
||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
||||||
|
screenCastPanel.setPluginId(getIdentifier());
|
||||||
bindShellWorkspaceContent(screenCastPanel);
|
bindShellWorkspaceContent(screenCastPanel);
|
||||||
installPeerActions(workspace.peerActions());
|
installPeerActions(workspace.peerActions());
|
||||||
}
|
}
|
||||||
@@ -80,17 +82,17 @@ public class ScreenCastPlugin extends Plugin {
|
|||||||
PeerActionTarget.ALL_PEERS,
|
PeerActionTarget.ALL_PEERS,
|
||||||
target -> {
|
target -> {
|
||||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||||
screenCastPanel.focusMulticastSharing();
|
screenCastPanel.focusMulticastSession();
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
registrar.register(PeerAction.of(
|
registrar.register(PeerAction.of(
|
||||||
"screencast.peer",
|
"screencast.peer",
|
||||||
"Compartilhar com este peer",
|
"Preparar compartilhamento com este peer",
|
||||||
PeerActionTarget.SINGLE_PEER,
|
PeerActionTarget.SINGLE_PEER,
|
||||||
target -> {
|
target -> {
|
||||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||||
if (target instanceof PeerTarget.Single single) {
|
if (target instanceof PeerTarget.Single single) {
|
||||||
screenCastPanel.focusPeerSharing(single.peer());
|
screenCastPanel.focusPeerAddress(single.peer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -146,11 +148,16 @@ public class ScreenCastPlugin extends Plugin {
|
|||||||
&& msg.getName() != null
|
&& msg.getName() != null
|
||||||
&& msg.getName().equals(view.getDisplayName())
|
&& msg.getName().equals(view.getDisplayName())
|
||||||
&& view.getInstanceId() != null) {
|
&& view.getInstanceId() != null) {
|
||||||
inbox.record(view.getInstanceId(), getIdentifier(), "screencast.peer");
|
inbox.signal(
|
||||||
|
view.getInstanceId(),
|
||||||
|
getIdentifier(),
|
||||||
|
"screencast.peer",
|
||||||
|
PeerActivityFocusTokens.peerInstance(view.getInstanceId())
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inbox.recordBroadcast(getIdentifier(), "screencast.multicast");
|
inbox.signalBroadcast(getIdentifier(), "screencast.multicast", PeerActivityFocusTokens.broadcast());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user