fix(limbo): 登录门聊天和 /link 回复里的网址可以直接点击打开
This commit is contained in:
8 files changed
+182
-22
No files matched your search
@@ -230,7 +230,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
|
||||
@Override
|
||||
public void chat(String line) {
|
||||
player.sendMessage(line);
|
||||
player.sendMessage(LoginBook.chatLine(line));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,14 +4,28 @@ import net.kyori.adventure.inventory.Book;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* LoginBook is the book the gate opens on join: the bind code, a line that opens the
|
||||
* login page when clicked, and the reminder to use the system browser, where
|
||||
* WebAuthn/passkey works (the WeChat/QQ in-app browsers break it).
|
||||
* LoginBook renders what the gate shows a player: the book it opens on join (the bind
|
||||
* code, a line that opens the login page when clicked, and the reminder to use the
|
||||
* system browser, where WebAuthn/passkey works; the WeChat/QQ in-app browsers break
|
||||
* it), and the chat lines, whose web addresses open when clicked.
|
||||
*/
|
||||
final class LoginBook {
|
||||
|
||||
// A web address in a chat line runs to the next space. The client makes nothing
|
||||
// in server chat clickable by itself, so a player who closed the book had to type
|
||||
// the login URL out by hand.
|
||||
private static final LegacyComponentSerializer CHAT = LegacyComponentSerializer.builder()
|
||||
.character(LegacyComponentSerializer.SECTION_CHAR)
|
||||
.extractUrls(Pattern.compile("https?://\\S+"), Style.style(NamedTextColor.AQUA, TextDecoration.UNDERLINED))
|
||||
.build();
|
||||
|
||||
private LoginBook() {
|
||||
}
|
||||
|
||||
@@ -26,4 +40,9 @@ final class LoginBook {
|
||||
.color(NamedTextColor.GRAY));
|
||||
return Book.book(Component.text("Felis Login"), Component.text("Felis"), page);
|
||||
}
|
||||
|
||||
/** chatLine renders one chat line with § colour codes, its web addresses clickable. */
|
||||
static Component chatLine(String line) {
|
||||
return CHAT.deserialize(line);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ final class LoginFlow {
|
||||
interface Seat {
|
||||
String name();
|
||||
|
||||
/** chat sends one line, with § colour codes. */
|
||||
/** chat sends one line, with § colour codes; a web address in it opens when clicked. */
|
||||
void chat(String line);
|
||||
|
||||
/** showCode opens the login book. It may throw for a client that refuses it. */
|
||||
|
||||
@@ -14,6 +14,7 @@ import net.kyori.adventure.inventory.Book;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@@ -119,6 +120,13 @@ public final class LoginFlowTest {
|
||||
List.of(CODE + " " + stub.panelUrl), seat.books);
|
||||
assertTrue("chat carries the code", seat.chat.get(0).contains(CODE));
|
||||
assertTrue("chat names the panel URL", seat.chat.get(1).contains(stub.panelUrl) && seat.chat.get(2).contains(stub.panelUrl));
|
||||
// The client makes nothing in server chat clickable by itself: a player who
|
||||
// closed the book had to type the URL out.
|
||||
for (int i = 1; i <= 2; i++) {
|
||||
List<ClickEvent> clicks = new ArrayList<>();
|
||||
collectClicks(LoginBook.chatLine(seat.chat.get(i)), clicks);
|
||||
assertEq("chat line " + i + " opens the panel URL", List.of(ClickEvent.openUrl(stub.panelUrl)), clicks);
|
||||
}
|
||||
assertEq("no release before the link", 0, seat.releases.size());
|
||||
|
||||
int before = stub.hits("/link/status/");
|
||||
@@ -357,6 +365,19 @@ public final class LoginFlowTest {
|
||||
collectClicks(page, clicks);
|
||||
assertEq("one click, opening the login page", List.of(ClickEvent.openUrl(CONSOLE + "/link")), clicks);
|
||||
assertTrue("the page says to use the system browser", plain(page).contains("SYSTEM browser"));
|
||||
|
||||
// A chat line keeps its text; only the address in it turns into a link.
|
||||
Component line = LoginBook.chatLine("§e[Felis] 打开 §b" + CONSOLE + "/link §e完成登录(勿用微信/QQ内置浏览器)。");
|
||||
assertEq("chat line text", "[Felis] 打开 " + CONSOLE + "/link 完成登录(勿用微信/QQ内置浏览器)。", plain(line));
|
||||
List<Component> links = new ArrayList<>();
|
||||
collectLinks(line, links);
|
||||
assertEq("one link in the line", 1, links.size());
|
||||
assertEq("the link is the address alone", CONSOLE + "/link", plain(links.get(0)));
|
||||
assertEq("the link opens it", ClickEvent.openUrl(CONSOLE + "/link"), links.get(0).clickEvent());
|
||||
assertEq("the link looks like one", TextDecoration.State.TRUE, links.get(0).decoration(TextDecoration.UNDERLINED));
|
||||
List<ClickEvent> none = new ArrayList<>();
|
||||
collectClicks(LoginBook.chatLine(LoginFlow.SIGNED_IN), none);
|
||||
assertEq("a line without an address has no link", List.of(), none);
|
||||
}
|
||||
|
||||
// ---- fakes ----
|
||||
@@ -642,6 +663,15 @@ public final class LoginFlowTest {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void collectLinks(Component c, List<Component> out) {
|
||||
if (c.clickEvent() != null) {
|
||||
out.add(c);
|
||||
}
|
||||
for (Component child : c.children()) {
|
||||
collectLinks(child, out);
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectClicks(Component c, List<ClickEvent> out) {
|
||||
if (c.clickEvent() != null) {
|
||||
out.add(c.clickEvent());
|
||||
|
||||
Reference in new issue
Block a user