Compare commits

...

30 Commits

Author SHA1 Message Date
Zlatin Balevsky
0c40c8f269 Release 0.6.6 2019-11-16 17:12:21 +00:00
Zlatin Balevsky
681ddb99a2 add ability to say something in a room 2019-11-16 17:10:21 +00:00
Zlatin Balevsky
5dff319746 prevent starting chat server more than once. Implement chat console in the cli 2019-11-16 16:40:07 +00:00
Zlatin Balevsky
57c4a00ac6 do not call disconnect() unless connecting/ed. This prevents trying to connect after closing the tab 2019-11-16 15:25:23 +00:00
Zlatin Balevsky
286a0a8678 redo locking around chat client state 2019-11-16 14:41:47 +00:00
Zlatin Balevsky
17eff7d77f toString 2019-11-16 00:25:50 +00:00
Zlatin Balevsky
2e22369ce0 set flag before submitting to threadpool 2019-11-15 23:12:17 +00:00
Zlatin Balevsky
15c59b440f flush outputstream on chat connect failures or rejections 2019-11-15 21:52:35 +00:00
Zlatin Balevsky
8fb015acbf sort browse host as well 2019-11-15 14:59:52 +00:00
Zlatin Balevsky
f7b11c90fd sort results from search 2019-11-15 14:49:15 +00:00
Zlatin Balevsky
df93a35062 wip on sorting 2019-11-15 14:23:00 +00:00
Zlatin Balevsky
ecb19a8412 do not update badge if the room is a console 2019-11-15 13:50:13 +00:00
Zlatin Balevsky
b1e5b40800 update minimum JDK to 9, version bump 2019-11-15 13:18:34 +00:00
Zlatin Balevsky
daa3a293f2 new messages update taskbar badge 2019-11-15 13:15:27 +00:00
Zlatin Balevsky
907264fc67 enable/disable chat and browse from trusted pane buttons 2019-11-15 02:15:56 +00:00
Zlatin Balevsky
c6becb93dc enable/disable say field when not connected 2019-11-15 01:57:48 +00:00
Zlatin Balevsky
2954bd2f1a smart scrolling the chat text area 2019-11-15 01:44:54 +00:00
Zlatin Balevsky
35322d2c15 fetch group by name,add sequential download checkbox to browse view 2019-11-14 12:40:40 +00:00
Zlatin Balevsky
9f6a7eb368 make sure browse window works from every parent group 2019-11-14 11:04:38 +00:00
Zlatin Balevsky
fec81808e5 Release 0.6.5 2019-11-14 05:15:26 +00:00
Zlatin Balevsky
4db890484d do not rejoin console 2019-11-14 04:49:13 +00:00
Zlatin Balevsky
dfd5e06889 add browse ability from chat room view 2019-11-14 04:40:15 +00:00
Zlatin Balevsky
71da8e14da name button earlier 2019-11-14 04:25:45 +00:00
Zlatin Balevsky
7dc37e3e0d change button to connect/disconnect 2019-11-14 04:20:57 +00:00
Zlatin Balevsky
3de058a078 send rejoins to the console pt2 2019-11-14 03:59:01 +00:00
Zlatin Balevsky
4d70c7adce send rejoins to the console 2019-11-14 03:58:36 +00:00
Zlatin Balevsky
5b41106476 start and stop poller thread on events 2019-11-14 03:45:21 +00:00
Zlatin Balevsky
6240b22e66 fix reconnecting to server, start with fresh member list upon rejoin 2019-11-14 03:13:01 +00:00
Zlatin Balevsky
0e26f5afd7 rejoin rooms on reconnect 2019-11-14 02:40:22 +00:00
Zlatin Balevsky
114bc06dbb If the user explicitly shares a file, remove it form the negative tree. #26 2019-11-13 22:00:10 +00:00
35 changed files with 920 additions and 76 deletions

View File

@@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.6.2 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.6.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew clean assemble

View File

@@ -72,4 +72,27 @@ class BrowseModel {
void setPercentageLabel(Label percentage) {
this.percentage = percentage
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
List<UIResultEvent> l = new ArrayList<>(rootToResult.values())
Collections.sort(l, chosen)
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
l.each { e ->
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment, e.certificates)
}
}
}

View File

@@ -58,11 +58,17 @@ class BrowseView extends BasicWindow {
}
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...", {sort()})
Button closeButton = new Button("Close",{
model.unregister()
close()
})
contentPanel.addComponent(closeButton, layoutData)
buttonsPanel.addComponent(sortButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
}
@@ -120,4 +126,11 @@ class BrowseView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -0,0 +1,88 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUIThread
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionEvent
import com.muwire.core.chat.ChatLink
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent
import net.i2p.data.DataHelper
class ChatConsoleModel {
private final Core core
private final TextGUIThread guiThread
volatile ChatLink link
volatile Thread poller
volatile boolean running
volatile TextBox textBox
ChatConsoleModel(Core core, TextGUIThread guiThread) {
this.core = core
this.guiThread = guiThread
}
void start() {
if (running)
return
running = true
core.chatServer.start()
core.eventBus.with {
register(ChatConnectionEvent.class, this)
publish(new UIConnectChatEvent(host : core.me))
}
}
void onChatConnectionEvent(ChatConnectionEvent e) {
if (e.persona != core.me)
return // can't really happen
link = e.connection
poller = new Thread({eventLoop()} as Runnable)
poller.setDaemon(true)
poller.start()
}
void stop() {
if (!running)
return
running = false
core.chatServer.stop()
poller?.interrupt()
link = null
}
private void eventLoop() {
Thread.sleep(1000)
while(running) {
ChatLink link = this.link
if (link == null || !link.isUp()) {
Thread.sleep(100)
continue
}
Object event = link.nextEvent()
if (event instanceof ChatMessageEvent)
handleChatMessage(event)
else if (event instanceof Persona)
handleLeave(event)
else
throw new IllegalArgumentException("unknown event type $event")
}
}
private void handleChatMessage(ChatMessageEvent e) {
String text = DataHelper.formatTime(e.timestamp)+" <"+e.sender.getHumanReadableName()+ "> ["+
e.room+"] "+e.payload
guiThread.invokeLater({textBox.addLine(text)})
}
private void handleLeave(Persona p) {
guiThread.invokeLater({textBox.addLine(p.getHumanReadableName()+ " disconnected")})
}
}

View File

@@ -0,0 +1,101 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.Core
import com.muwire.core.chat.ChatConnection
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.ChatServer
import net.i2p.data.DataHelper
class ChatConsoleView extends BasicWindow {
private final TextGUI textGUI
private final ChatConsoleModel model
private final Core core
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
private final TextBox textBox
private final TextBox sayField
private final TextBox roomField
ChatConsoleView(Core core, ChatConsoleModel model, TextGUI textGUI, TerminalSize terminalSize) {
super("Chat Server Console")
this.core = core
this.model = model
this.textGUI = textGUI
this.textBox = new TextBox(terminalSize,"", TextBox.Style.MULTI_LINE)
model.textBox = textBox
model.start()
this.sayField = new TextBox("", TextBox.Style.SINGLE_LINE)
this.roomField = new TextBox("__CONSOLE__", TextBox.Style.SINGLE_LINE)
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
contentPanel.addComponent(textBox, layoutData)
Panel inputPanel = new Panel()
inputPanel.with {
setLayoutManager(new GridLayout(2))
addComponent(new Label("Say something here"), layoutData)
addComponent(sayField, layoutData)
addComponent(new Label("In room:"), layoutData)
addComponent(roomField, layoutData)
}
contentPanel.addComponent(inputPanel, layoutData)
Panel bottomPanel = new Panel()
bottomPanel.setLayoutManager(new GridLayout(4))
Button sayButton = new Button("Say",{say()})
Button startButton = new Button("Start Server",{model.start()})
Button stopButton = new Button("Stop Server", {model.stop()})
Button closeButton = new Button("Close",{close()})
bottomPanel.with {
addComponent(sayButton, layoutData)
addComponent(startButton, layoutData)
addComponent(stopButton, layoutData)
addComponent(closeButton, layoutData)
}
contentPanel.addComponent(bottomPanel, layoutData)
setComponent(contentPanel)
}
private void say() {
String command = sayField.getText()
sayField.setText("")
String room = roomField.getText()
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
String toAppend = DataHelper.formatTime(now) + " <" + core.me.getHumanReadableName() + "> [$room] " + command
textBox.addLine(toAppend)
byte[] sig = ChatConnection.sign(uuid, now, room, command, core.me, core.me, core.spk)
def event = new ChatMessageEvent( uuid : uuid,
payload : command,
sender : core.me,
host : core.me,
room : room,
chatTime : now,
sig : sig
)
core.eventBus.publish(event)
}
}

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.6.4"
private static final String MW_VERSION = "0.6.6"
private static volatile Core core

View File

@@ -78,4 +78,32 @@ class FilesModel {
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
}
}
private void sort(SortType type) {
Comparator<SharedFile> chosen
switch(type) {
case SortType.NAME_ASC : chosen = NAME_ASC; break
case SortType.NAME_DESC : chosen = NAME_DESC; break
case SortType.SIZE_ASC : chosen = SIZE_ASC; break
case SortType.SIZE_DESC : chosen = SIZE_DESC; break
}
Collections.sort(sharedFiles, chosen)
}
private static final Comparator<SharedFile> NAME_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
a.getFile().getName().compareTo(b.getFile().getName())
}
}
private static final Comparator<SharedFile> NAME_DESC = NAME_ASC.reversed()
private static final Comparator<SharedFile> SIZE_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
Long.compare(a.getCachedLength(), b.getCachedLength())
}
}
private static final Comparator<SharedFile> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@@ -51,17 +51,19 @@ class FilesView extends BasicWindow {
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(4))
buttonsPanel.setLayoutManager(new GridLayout(5))
Button shareFile = new Button("Share File", {shareFile()})
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
Button sort = new Button("Sort...",{sort()})
Button close = new Button("Close", {close()})
buttonsPanel.with {
addComponent(shareFile, layoutData)
addComponent(shareDirectory, layoutData)
addComponent(unshareDirectory, layoutData)
addComponent(sort, layoutData)
addComponent(close, layoutData)
}
@@ -134,4 +136,11 @@ class FilesView extends BasicWindow {
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -44,6 +44,7 @@ class MainWindowView extends BasicWindow {
private final UploadsModel uploadsModel
private final FilesModel filesModel
private final TrustModel trustModel
private final ChatConsoleModel chatModel
private final Label connectionCount, incoming, outgoing
private final Label known, failing, hopeless
@@ -63,6 +64,9 @@ class MainWindowView extends BasicWindow {
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
filesModel = new FilesModel(textGUI.getGUIThread(),core)
trustModel = new TrustModel(textGUI.getGUIThread(), core)
chatModel = new ChatConsoleModel(core, textGUI.getGUIThread())
if (core.muOptions.startChatServer)
core.chatServer.start()
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
@@ -74,7 +78,7 @@ class MainWindowView extends BasicWindow {
Panel buttonsPanel = new Panel()
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
GridLayout gridLayout = new GridLayout(7)
GridLayout gridLayout = new GridLayout(8)
buttonsPanel.setLayoutManager(gridLayout)
searchTextBox = new TextBox(new TerminalSize(40, 1))
@@ -83,6 +87,7 @@ class MainWindowView extends BasicWindow {
Button uploadsButton = new Button("Uploads", {upload()})
Button filesButton = new Button("Files", { files() })
Button trustButton = new Button("Trust", {trust()})
Button chatButton = new Button("Chat", {chat()})
Button quitButton = new Button("Quit", {close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
@@ -94,6 +99,7 @@ class MainWindowView extends BasicWindow {
addComponent(uploadsButton, layoutData)
addComponent(filesButton, layoutData)
addComponent(trustButton, layoutData)
addComponent(chatButton, layoutData)
addComponent(quitButton, layoutData)
}
@@ -271,6 +277,10 @@ class MainWindowView extends BasicWindow {
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
}
private void chat() {
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
}
private void refreshStats() {
int inCon = 0
int outCon = 0

View File

@@ -0,0 +1,21 @@
package com.muwire.clilanterna
import com.muwire.core.search.UIResultEvent
class ResultComparators {
public static final Comparator<UIResultEvent> NAME_ASC = new Comparator<UIResultEvent>() {
public int compare(UIResultEvent a, UIResultEvent b) {
a.name.compareTo(b.name)
}
}
public static final Comparator<UIResultEvent> NAME_DESC = NAME_ASC.reversed()
public static final Comparator<UIResultEvent> SIZE_ASC = new Comparator<UIResultEvent>() {
public int compare(UIResultEvent a, UIResultEvent b) {
Long.compare(a.size, b.size)
}
}
public static final Comparator<UIResultEvent> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@@ -16,7 +16,27 @@ class ResultsModel {
ResultsModel(UIResultBatchEvent results) {
this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
results.results.each {
updateModel()
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
Arrays.sort(results.results, chosen)
updateModel()
}
private void updateModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot())
String sources = String.valueOf(it.sources.size())

View File

@@ -43,9 +43,14 @@ class ResultsView extends BasicWindow {
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...",{sort()})
buttonsPanel.addComponent(sortButton)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
buttonsPanel.addComponent(closeButton)
contentPanel.addComponent(buttonsPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
@@ -109,4 +114,11 @@ class ResultsView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -0,0 +1,57 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
class SortPrompt extends BasicWindow {
private final TextGUI textGUI
private SortType type
SortPrompt(TextGUI textGUI) {
super("Select what to sort by")
this.textGUI = textGUI
}
SortType prompt() {
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(5))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button nameAsc = new Button("Name (ascending)",{
type = SortType.NAME_ASC
close()
})
Button nameDesc = new Button("Name (descending)",{
type = SortType.NAME_DESC
close()
})
Button sizeAsc = new Button("Size (ascending)",{
type = SortType.SIZE_ASC
close()
})
Button sizeDesc = new Button("Size (descending)",{
type = SortType.SIZE_DESC
close()
})
Button close = new Button("Cancel",{close()})
contentPanel.with {
addComponent(nameAsc, layoutData)
addComponent(nameDesc, layoutData)
addComponent(sizeAsc, layoutData)
addComponent(sizeDesc, layoutData)
addComponent(close, layoutData)
}
setComponent(contentPanel)
textGUI.addWindowAndWait(this)
type
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.clilanterna;
public enum SortType {
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
}

View File

@@ -436,7 +436,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.6.4")
Core core = new Core(props, home, "0.6.6")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -1,20 +1,24 @@
package com.muwire.core.chat;
enum ChatAction {
JOIN(true, false, true),
LEAVE(false, false, true),
SAY(false, false, true),
LIST(true, true, true),
HELP(true, true, true),
INFO(true, true, true),
JOINED(true, true, false);
JOIN(true, false, true, false),
LEAVE(false, false, true, false),
SAY(false, false, true, false),
LIST(true, true, true, false),
HELP(true, true, true, false),
INFO(true, true, true, false),
JOINED(true, true, false, false),
TRUST(true, false, true, true),
DISTRUST(true, false, true, true);
final boolean console;
final boolean stateless;
final boolean user;
ChatAction(boolean console, boolean stateless, boolean user) {
final boolean local;
ChatAction(boolean console, boolean stateless, boolean user, boolean local) {
this.console = console;
this.stateless = stateless;
this.user = user;
this.local = local;
}
}

View File

@@ -1,5 +1,6 @@
package com.muwire.core.chat
import java.lang.System.Logger.Level
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@@ -28,10 +29,10 @@ class ChatClient implements Closeable {
private final TrustService trustService
private final MuWireSettings settings
private volatile ChatConnection connection
private volatile boolean connectInProgress
private volatile long lastRejectionTime
private volatile Thread connectThread
private ChatConnection connection
private boolean connectInProgress
private long lastRejectionTime
private Thread connectThread
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
MuWireSettings settings) {
@@ -43,15 +44,19 @@ class ChatClient implements Closeable {
this.settings = settings
}
void connectIfNeeded() {
synchronized void connectIfNeeded() {
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
return
connectInProgress = true
CONNECTOR.execute({connect()})
}
private void connect() {
connectInProgress = true
connectThread = Thread.currentThread()
synchronized(this) {
if (!connectInProgress)
return
connectThread = Thread.currentThread()
}
Endpoint endpoint = null
try {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
@@ -72,8 +77,11 @@ class ChatClient implements Closeable {
if (code == 429) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
try { dos.close() } catch (IOException ignore) {}
endpoint.close()
lastRejectionTime = System.currentTimeMillis()
synchronized(this) {
lastRejectionTime = System.currentTimeMillis()
}
return
}
@@ -88,32 +96,47 @@ class ChatClient implements Closeable {
if (version != Constants.CHAT_VERSION)
throw new Exception("Unknown chat version $version")
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
connection.start()
synchronized(this) {
if (!connectInProgress)
return
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
connection.start()
}
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
connection : connection))
} catch (Exception e) {
log.log(java.util.logging.Level.WARNING, "connect failed", e)
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
endpoint?.close()
if (endpoint != null) {
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
endpoint.close()
}
} finally {
connectInProgress = false
connectThread = null
synchronized(this) {
connectInProgress = false
connectThread = null
}
}
}
void disconnected() {
synchronized void disconnected() {
connectInProgress = false
connection = null
}
@Override
public void close() {
synchronized public void close() {
connectInProgress = false
connectThread?.interrupt()
connection?.close()
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
}
void ping() {
synchronized void ping() {
connection?.sendPing()
}
synchronized void sendChat(ChatMessageEvent e) {
connection?.sendChat(e)
}
}

View File

@@ -109,6 +109,7 @@ class ChatConnection implements ChatLink {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader", e)
} finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close()
}
}
@@ -123,6 +124,7 @@ class ChatConnection implements ChatLink {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in writer",e)
} finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close()
}
}

View File

@@ -7,4 +7,8 @@ class ChatConnectionEvent extends Event {
ChatConnectionAttemptStatus status
Persona persona
ChatLink connection
public String toString() {
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
}
}

View File

@@ -51,7 +51,7 @@ class ChatManager {
return
if (e.sender != me)
return
clients[e.host]?.connection?.sendChat(e)
clients[e.host]?.sendChat(e)
}
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {

View File

@@ -34,6 +34,7 @@ class ChatServer {
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
private final Map<Persona, Set<String>> memberships = new ConcurrentHashMap<>()
private final Map<String, Persona> shortNames = new ConcurrentHashMap<>()
private final AtomicBoolean running = new AtomicBoolean()
@@ -49,9 +50,11 @@ class ChatServer {
}
public void start() {
running.set(true)
if (!running.compareAndSet(false, true))
return
connections.put(me.destination, LocalChatLink.INSTANCE)
joinRoom(me, CONSOLE)
shortNames.put(me.getHumanReadableName(), me)
echo("/SAY Welcome to my chat server! Type /HELP for list of available commands.",me.destination)
}
@@ -105,6 +108,7 @@ class ChatServer {
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
connections.put(endpoint.destination, connection)
joinRoom(client, CONSOLE)
shortNames.put(client.getHumanReadableName(), client)
connection.start()
echo("/SAY Welcome to my chat server! Type /HELP for help on available commands",connection.endpoint.destination)
}
@@ -120,6 +124,7 @@ class ChatServer {
leaveRoom(e.persona, it)
}
}
shortNames.remove(e.persona.getHumanReadableName())
connections.each { k, v ->
v.sendLeave(e.persona)
}
@@ -187,6 +192,9 @@ class ChatServer {
(!command.action.console && e.room == CONSOLE) ||
!command.action.user)
return
if (command.action.local && e.sender != me)
return
switch(command.action) {
case ChatAction.JOIN : processJoin(command.payload, e); break
@@ -195,6 +203,8 @@ class ChatServer {
case ChatAction.LIST : processList(e.sender.destination); break
case ChatAction.INFO : processInfo(e.sender.destination); break
case ChatAction.HELP : processHelp(e.sender.destination); break
case ChatAction.TRUST : processTrust(command.payload, TrustLevel.TRUSTED); break
case ChatAction.DISTRUST : processTrust(command.payload, TrustLevel.DISTRUSTED); break
}
}
@@ -264,12 +274,14 @@ class ChatServer {
private void processHelp(Destination d) {
String help = """/SAY
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /HELP
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /TRUST /DISTRUST /HELP
/JOIN <room name> - joins a room, or creates one if it does not exist. You must type this in the console
/LEAVE - leaves a room. You must type this in the room you want to leave
/SAY - optional, says something in the room you're in
/LIST - lists the existing rooms on this server. You must type this in the console
/INFO - shows information about this server. You must type this in the console
/TRUST <user> - marks user as trusted. This is only available to the server owner
/DISTRUST <user> - marks user as distrusted. This is only available to the server owner
/HELP - prints this help message
"""
echo(help, d)
@@ -292,6 +304,13 @@ class ChatServer {
connections[d]?.sendChat(echo)
}
private void processTrust(String shortName, TrustLevel level) {
Persona p = shortNames.get(shortName)
if (p == null)
return
eventBus.publish(new TrustEvent(persona : p, level : level))
}
void stop() {
if (running.compareAndSet(true, false)) {
connections.each { k, v ->

View File

@@ -7,7 +7,7 @@ class FileTree {
private final TreeNode root = new TreeNode()
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
void add(File file) {
synchronized void add(File file) {
List<File> path = new ArrayList<>()
path.add(file)
while (file.getParentFile() != null) {
@@ -31,7 +31,7 @@ class FileTree {
}
}
boolean remove(File file) {
synchronized boolean remove(File file) {
TreeNode node = fileToNode.remove(file)
if (node == null) {
return false

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.6.4
version = 0.6.6
i2pVersion = 0.9.43
groovyVersion = 2.4.15
slf4jVersion = 1.7.25

View File

@@ -64,8 +64,11 @@ class BrowseController {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty())
return
def group = application.mvcGroupManager.getGroups()['MainFrame']
selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
!group.model.canDownload(it.infohash)
}
selectedResults.each { result ->
@@ -74,11 +77,11 @@ class BrowseController {
result : [result],
sources : [model.host.destination],
target : file,
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
sequential : view.sequentialDownloadCheckbox.model.isSelected()
))
}
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
group.view.showDownloadsWindow.call()
dismiss()
}

View File

@@ -71,6 +71,7 @@ class ChatRoomController {
params['console'] = false
params['host'] = model.host
params['roomTabName'] = newRoom
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", model.host.getHumanReadableName()+"-"+newRoom, params)
}
@@ -110,6 +111,7 @@ class ChatRoomController {
params['privateChat'] = true
params['host'] = model.host
params['roomTabName'] = p.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
}
@@ -141,6 +143,17 @@ class ChatRoomController {
view.refreshMembersTable()
}
void browse() {
Persona p = view.getSelectedPersona()
if (p == null)
return
String groupId = p.getHumanReadableName() + "-browse"
def params = [:]
params['host'] = p
params['core'] = model.core
mvcGroup.createMVCGroup("browse",groupId,params)
}
void leaveRoom() {
if (leftRoom)
return
@@ -180,6 +193,8 @@ class ChatRoomController {
runInsideUIAsync {
view.roomTextArea.append(toDisplay)
trimLines()
if (!model.console)
view.chatNotificator.onMessage(mvcGroup.mvcId)
}
}
@@ -233,4 +248,27 @@ class ChatRoomController {
view.roomTextArea.replaceRange(null, line0Start, line0End)
}
}
void rejoinRoom() {
if (model.room == "Console")
return
model.members.clear()
model.members.add(model.core.me)
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
String join = "/JOIN $model.room"
byte [] sig = ChatConnection.sign(uuid, now, ChatServer.CONSOLE, join, model.core.me, model.host, model.core.spk)
def event = new ChatMessageEvent(
uuid : uuid,
payload : join,
sender : model.core.me,
host : model.host,
room : ChatServer.CONSOLE,
chatTime : now,
sig : sig
)
model.core.eventBus.publish(event)
}
}

View File

@@ -15,6 +15,14 @@ class ChatServerController {
@ControllerAction
void disconnect() {
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
switch(model.buttonText) {
case "Disconnect" :
model.buttonText = "Connect"
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
break
case "Connect" :
model.connect()
break
}
}
}

View File

@@ -458,6 +458,7 @@ class MainFrameController {
def params = [:]
params['core'] = model.core
params['host'] = model.core.me
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
}
}
@@ -489,8 +490,10 @@ class MainFrameController {
def params = [:]
params['core'] = model.core
params['host'] = p
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
}
} else
mvcGroup.getChildrenGroups().get(p.getHumanReadableName()).model.connect()
}
void saveMuWireSettings() {

View File

@@ -2,6 +2,8 @@ package com.muwire.gui
import java.util.logging.Level
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.chat.ChatCommand
@@ -13,6 +15,7 @@ import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import groovy.util.logging.Log
import griffon.metadata.ArtifactProviderFor
@@ -20,11 +23,16 @@ import griffon.metadata.ArtifactProviderFor
@Log
@ArtifactProviderFor(GriffonModel)
class ChatServerModel {
@MVCMember @Nonnull
ChatServerView view
Persona host
Core core
@Observable boolean disconnectActionEnabled
@Observable String buttonText = "Disconnect"
@Observable ChatConnectionAttemptStatus status
@Observable boolean sayActionEnabled
volatile ChatLink link
volatile Thread poller
@@ -32,40 +40,65 @@ class ChatServerModel {
void mvcGroupInit(Map<String, String> params) {
disconnectActionEnabled = host != core.me // can't disconnect from myself
core.eventBus.with {
register(ChatConnectionEvent.class, this)
publish(new UIConnectChatEvent(host : host))
core.eventBus.register(ChatConnectionEvent.class, this)
connect()
}
void connect() {
runInsideUIAsync {
buttonText = "Disconnect"
}
core.eventBus.publish(new UIConnectChatEvent(host : host))
}
void mvcGroupDestroy() {
stopPoller()
core.eventBus.unregister(ChatConnectionEvent.class, this)
}
private void startPoller() {
if (running)
return
running = true
poller = new Thread({eventLoop()} as Runnable)
poller.setDaemon(true)
poller.start()
}
void mvcGroupDestroy() {
private void stopPoller() {
running = false
poller?.interrupt()
link = null
}
void onChatConnectionEvent(ChatConnectionEvent e) {
if (e.persona == host) {
runInsideUIAsync {
status = e.status
}
}
ChatLink link = e.connection
if (link == null)
if (e.persona != host)
return
if (link.getPersona() == host)
this.link = link
else if (link.getPersona() == null && host == core.me)
this.link = link
runInsideUIAsync {
status = e.status
sayActionEnabled = status == ChatConnectionAttemptStatus.SUCCESSFUL
}
if (e.status == ChatConnectionAttemptStatus.SUCCESSFUL) {
ChatLink link = e.connection
if (link == null)
return
this.link = e.connection
startPoller()
mvcGroup.childrenGroups.each {k,v ->
v.controller.rejoinRoom()
}
} else {
stopPoller()
}
}
private void eventLoop() {
Thread.sleep(1000)
while(running) {
ChatLink link = this.link
if (link == null || !link.isUp()) {
@@ -106,6 +139,7 @@ class ChatServerModel {
params['privateChat'] = true
params['host'] = host
params['roomTabName'] = e.sender.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-room",groupId, params)
}

View File

@@ -106,6 +106,8 @@ class MainFrameModel {
@Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled
@Observable boolean browseFromTrustedButtonEnabled
@Observable boolean chatFromTrustedButtonEnabled
@Observable boolean markNeutralFromDistrustedButtonEnabled
@Observable boolean markTrustedButtonEnabled
@Observable boolean reviewButtonEnabled

View File

@@ -39,6 +39,8 @@ class BrowseView {
def p
def resultsTable
def lastSortEvent
def sequentialDownloadCheckbox
void initUI() {
int rowHeight = application.context.get("row-height")
mainFrame = application.windowManager.findWindow("main-frame")
@@ -67,6 +69,8 @@ class BrowseView {
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
button(text : "Dismiss", dismissAction)
label(text : "Download sequentially")
sequentialDownloadCheckbox = checkBox()
}
}
@@ -106,8 +110,9 @@ class BrowseView {
else
model.viewCommentActionEnabled = false
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
downloadActionEnabled &= mainFrameGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled

View File

@@ -12,6 +12,7 @@ import javax.swing.SwingConstants
import javax.swing.SpringLayout.Constraints
import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionAttemptStatus
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
@@ -28,29 +29,33 @@ class ChatRoomView {
@MVCMember @Nonnull
ChatRoomController controller
ChatNotificator chatNotificator
def pane
def parent
def sayField
def roomTextArea
def textScrollPane
def membersTable
def lastMembersTableSortEvent
void initUI() {
int rowHeight = application.context.get("row-height")
def parentModel = mvcGroup.parentGroup.model
if (model.console || model.privateChat) {
pane = builder.panel {
borderLayout()
panel(constraints : BorderLayout.CENTER) {
gridLayout(rows : 1, cols : 1)
scrollPane {
textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
}
}
panel(constraints : BorderLayout.SOUTH) {
borderLayout()
label(text : "Say something here: ", constraints : BorderLayout.WEST)
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(text : "Say", constraints : BorderLayout.EAST, sayAction)
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(enabled : bind {parentModel.sayActionEnabled},text : "Say", constraints : BorderLayout.EAST, sayAction)
}
}
} else {
@@ -72,7 +77,7 @@ class ChatRoomView {
}
panel {
gridLayout(rows : 1, cols : 1)
scrollPane {
textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
}
}
@@ -81,12 +86,15 @@ class ChatRoomView {
panel(constraints : BorderLayout.SOUTH) {
borderLayout()
label(text : "Say something here: ", constraints : BorderLayout.WEST)
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(text : "Say", constraints : BorderLayout.EAST, sayAction)
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(enabled : bind {parentModel.sayActionEnabled}, text : "Say", constraints : BorderLayout.EAST, sayAction)
}
}
}
SmartScroller smartScroller = new SmartScroller(textScrollPane)
pane.putClientProperty("mvcId", mvcGroup.mvcId)
}
void mvcGroupInit(Map<String,String> args) {
@@ -134,6 +142,9 @@ class ChatRoomView {
JMenuItem privateChat = new JMenuItem("Start Private Chat")
privateChat.addActionListener({controller.privateMessage()})
menu.add(privateChat)
JMenuItem browse = new JMenuItem("Browse")
browse.addActionListener({controller.browse()})
menu.add(browse)
JMenuItem markTrusted = new JMenuItem("Mark Trusted")
markTrusted.addActionListener({controller.markTrusted()})
menu.add(markTrusted)
@@ -165,6 +176,7 @@ class ChatRoomView {
int index = parent.indexOfComponent(pane)
parent.removeTabAt(index)
controller.leaveRoom()
chatNotificator.roomClosed(mvcGroup.mvcId)
mvcGroup.destroy()
}
}

View File

@@ -19,19 +19,22 @@ class ChatServerView {
ChatServerModel model
@MVCMember @Nonnull
ChatServerController controller
ChatNotificator chatNotificator
def pane
def parent
def childPane
void initUI() {
pane = builder.panel {
borderLayout()
tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER)
childPane = tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows : 1, cols : 3)
panel {}
panel {
button(text : "Disconnect", enabled : bind {model.disconnectActionEnabled}, disconnectAction)
button(text : bind {model.buttonText}, enabled : bind {model.disconnectActionEnabled}, disconnectAction)
}
panel {
label(text : "Connection Status ")
@@ -39,6 +42,9 @@ class ChatServerView {
}
}
}
pane.putClientProperty("mvcId",mvcGroup.mvcId)
pane.putClientProperty("childPane", childPane)
childPane.addChangeListener({e -> chatNotificator.roomTabChanged(e.getSource())})
}
void mvcGroupInit(Map<String,String> args) {
@@ -69,11 +75,13 @@ class ChatServerView {
params['roomTabName'] = 'Console'
params['console'] = true
params['host'] = model.host
params['chatNotificator'] = chatNotificator
mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params)
}
def closeTab = {
controller.disconnect()
if (model.buttonText == "Disconnect")
controller.disconnect()
int index = parent.indexOfComponent(pane)
parent.removeTabAt(index)
mvcGroup.destroy()

View File

@@ -78,8 +78,10 @@ class MainFrameView {
UISettings settings
ChatNotificator chatNotificator
void initUI() {
chatNotificator = new ChatNotificator(application.getMvcGroupManager())
settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height")
builder.with {
@@ -434,8 +436,8 @@ class MainFrameView {
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction)
button(text : "Mark Neutral", enabled : bind {model.markNeutralFromTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", enabled : bind {model.markDistrustedButtonEnabled}, constraints : gbc(gridx: 2, gridy:0), markDistrustedAction)
button(text : "Browse", constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
button(text : "Chat", constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
button(text : "Browse", enabled : bind{model.browseFromTrustedButtonEnabled}, constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
button(text : "Chat", enabled : bind{model.chatFromTrustedButtonEnabled} ,constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
}
}
panel (border : etchedBorder()){
@@ -511,7 +513,9 @@ class MainFrameView {
public boolean importData(TransferHandler.TransferSupport support) {
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
files.each {
model.core.eventBus.publish(new FileSharedEvent(file : it))
File canonical = it.getCanonicalFile()
model.core.fileManager.negativeTree.remove(canonical)
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
}
showUploadsWindow.call()
true
@@ -520,6 +524,7 @@ class MainFrameView {
mainFrame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
chatNotificator.mainWindowDeactivated()
if (application.getContext().get("tray-icon")) {
if (settings.closeWarning) {
runInsideUIAsync {
@@ -533,6 +538,13 @@ class MainFrameView {
} else {
closeApplication()
}
}
public void windowDeactivated(WindowEvent e) {
chatNotificator.mainWindowDeactivated()
}
public void windowActivated(WindowEvent e) {
if (!model.chatPaneButtonEnabled)
chatNotificator.mainWindowActivated()
}})
// search field
@@ -773,10 +785,14 @@ class MainFrameView {
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
model.chatFromTrustedButtonEnabled = false
model.browseFromTrustedButtonEnabled = false
} else {
model.subscribeButtonEnabled = true
model.markDistrustedButtonEnabled = true
model.markNeutralFromTrustedButtonEnabled = true
model.chatFromTrustedButtonEnabled = true
model.browseFromTrustedButtonEnabled = true
}
})
@@ -825,6 +841,10 @@ class MainFrameView {
}
})
// chat tabs
def chatTabbedPane = builder.getVariable("chat-tabs")
chatTabbedPane.addChangeListener({e -> chatNotificator.serverTabChanged(e.getSource())})
// show tree by default
showSharedFilesTree.call()
@@ -1027,6 +1047,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showDownloadsWindow = {
@@ -1038,6 +1059,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showUploadsWindow = {
@@ -1049,6 +1071,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showMonitorWindow = {
@@ -1060,6 +1083,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = false
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showTrustWindow = {
@@ -1071,6 +1095,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = false
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showChatWindow = {
@@ -1082,6 +1107,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = false
chatNotificator.mainWindowActivated()
}
def showSharedFilesTable = {
@@ -1106,6 +1132,7 @@ class MainFrameView {
if (rv == JFileChooser.APPROVE_OPTION) {
chooser.getSelectedFiles().each {
File canonical = it.getCanonicalFile()
model.core.fileManager.negativeTree.remove(canonical)
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
}
}

View File

@@ -0,0 +1,91 @@
package com.muwire.gui
import java.awt.Taskbar
import java.awt.Taskbar.Feature
import javax.swing.JPanel
import javax.swing.JTabbedPane
import griffon.core.mvc.MVCGroupManager
class ChatNotificator {
private final MVCGroupManager groupManager
private boolean chatInFocus
private String currentServerTab
private String currentRoomTab
private final Map<String, Integer> roomsWithMessages = new HashMap<>()
ChatNotificator(MVCGroupManager groupManager) {
this.groupManager = groupManager
}
void serverTabChanged(JTabbedPane source) {
JPanel panel = source.getSelectedComponent()
String mvcId = panel.getClientProperty("mvcId")
def group = groupManager.getGroups().get(mvcId)
JTabbedPane childPane = panel.getClientProperty("childPane")
JPanel roomPanel = childPane.getSelectedComponent()
currentServerTab = mvcId
currentRoomTab = childPane.getSelectedComponent()?.getClientProperty("mvcId")
if (currentRoomTab != null) {
roomsWithMessages.remove(currentRoomTab)
updateBadge()
}
}
void roomTabChanged(JTabbedPane source) {
JPanel panel = source.getSelectedComponent()
currentRoomTab = panel.getClientProperty("mvcId")
roomsWithMessages.remove(currentRoomTab)
updateBadge()
}
void roomClosed(String mvcId) {
roomsWithMessages.remove(mvcId)
updateBadge()
}
void mainWindowDeactivated() {
chatInFocus = false
}
void mainWindowActivated() {
chatInFocus = true
if (currentRoomTab != null)
roomsWithMessages.remove(currentRoomTab)
updateBadge()
}
void onMessage(String roomId) {
if (roomId != currentRoomTab || !chatInFocus) {
Integer previous = roomsWithMessages[roomId]
if (previous == null)
roomsWithMessages[roomId] = 1
else
roomsWithMessages[roomId] = previous + 1
}
updateBadge()
}
private void updateBadge() {
if (!Taskbar.isTaskbarSupported())
return
def taskBar = Taskbar.getTaskbar()
if (!taskBar.isSupported(Feature.ICON_BADGE_NUMBER))
return
if (roomsWithMessages.isEmpty())
taskBar.setIconBadge("")
else {
int total = 0
roomsWithMessages.values().each {
total += it
}
taskBar.setIconBadge(String.valueOf(total))
}
}
}

View File

@@ -0,0 +1,174 @@
package com.muwire.gui;
import java.awt.Component;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
/**
* The SmartScroller will attempt to keep the viewport positioned based on
* the users interaction with the scrollbar. The normal behaviour is to keep
* the viewport positioned to see new data as it is dynamically added.
*
* Assuming vertical scrolling and data is added to the bottom:
*
* - when the viewport is at the bottom and new data is added,
* then automatically scroll the viewport to the bottom
* - when the viewport is not at the bottom and new data is added,
* then do nothing with the viewport
*
* Assuming vertical scrolling and data is added to the top:
*
* - when the viewport is at the top and new data is added,
* then do nothing with the viewport
* - when the viewport is not at the top and new data is added, then adjust
* the viewport to the relative position it was at before the data was added
*
* Similiar logic would apply for horizontal scrolling.
*/
public class SmartScroller implements AdjustmentListener
{
public final static int HORIZONTAL = 0;
public final static int VERTICAL = 1;
public final static int START = 0;
public final static int END = 1;
private int viewportPosition;
private JScrollBar scrollBar;
private boolean adjustScrollBar = true;
private int previousValue = -1;
private int previousMaximum = -1;
/**
* Convenience constructor.
* Scroll direction is VERTICAL and viewport position is at the END.
*
* @param scrollPane the scroll pane to monitor
*/
public SmartScroller(JScrollPane scrollPane)
{
this(scrollPane, VERTICAL, END);
}
/**
* Convenience constructor.
* Scroll direction is VERTICAL.
*
* @param scrollPane the scroll pane to monitor
* @param viewportPosition valid values are START and END
*/
public SmartScroller(JScrollPane scrollPane, int viewportPosition)
{
this(scrollPane, VERTICAL, viewportPosition);
}
/**
* Specify how the SmartScroller will function.
*
* @param scrollPane the scroll pane to monitor
* @param scrollDirection indicates which JScrollBar to monitor.
* Valid values are HORIZONTAL and VERTICAL.
* @param viewportPosition indicates where the viewport will normally be
* positioned as data is added.
* Valid values are START and END
*/
public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition)
{
if (scrollDirection != HORIZONTAL
&& scrollDirection != VERTICAL)
throw new IllegalArgumentException("invalid scroll direction specified");
if (viewportPosition != START
&& viewportPosition != END)
throw new IllegalArgumentException("invalid viewport position specified");
this.viewportPosition = viewportPosition;
if (scrollDirection == HORIZONTAL)
scrollBar = scrollPane.getHorizontalScrollBar();
else
scrollBar = scrollPane.getVerticalScrollBar();
scrollBar.addAdjustmentListener( this );
// Turn off automatic scrolling for text components
Component view = scrollPane.getViewport().getView();
if (view instanceof JTextComponent)
{
JTextComponent textComponent = (JTextComponent)view;
DefaultCaret caret = (DefaultCaret)textComponent.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
}
}
@Override
public void adjustmentValueChanged(final AdjustmentEvent e)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
checkScrollBar(e);
}
});
}
/*
* Analyze every adjustment event to determine when the viewport
* needs to be repositioned.
*/
private void checkScrollBar(AdjustmentEvent e)
{
// The scroll bar listModel contains information needed to determine
// whether the viewport should be repositioned or not.
JScrollBar scrollBar = (JScrollBar)e.getSource();
BoundedRangeModel listModel = scrollBar.getModel();
int value = listModel.getValue();
int extent = listModel.getExtent();
int maximum = listModel.getMaximum();
boolean valueChanged = previousValue != value;
boolean maximumChanged = previousMaximum != maximum;
// Check if the user has manually repositioned the scrollbar
if (valueChanged && !maximumChanged)
{
if (viewportPosition == START)
adjustScrollBar = value != 0;
else
adjustScrollBar = value + extent >= maximum;
}
// Reset the "value" so we can reposition the viewport and
// distinguish between a user scroll and a program scroll.
// (ie. valueChanged will be false on a program scroll)
if (adjustScrollBar && viewportPosition == END)
{
// Scroll the viewport to the end.
scrollBar.removeAdjustmentListener( this );
value = maximum - extent;
scrollBar.setValue( value );
scrollBar.addAdjustmentListener( this );
}
if (adjustScrollBar && viewportPosition == START)
{
// Keep the viewport at the same relative viewportPosition
scrollBar.removeAdjustmentListener( this );
value = value + maximum - previousMaximum;
scrollBar.setValue( value );
scrollBar.addAdjustmentListener( this );
}
previousValue = value;
previousMaximum = maximum;
}
}