From 60865aaaf290093ca1571744c5ef2067550c7dc3 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Sat, 24 Oct 2020 02:13:16 -0400 Subject: [PATCH] Updates public servers design and api Reworks the design of the public servers module to be more like Discord's server discovery feature. Updates some of the API and terminology to match that of DiscordServers.com --- src/data/changelog.js | 3 +- src/data/strings.js | 5 +- src/structs/psconnection.js | 19 +- src/styles/builtins/publicservers.css | 338 ++++++++++++++------------ src/ui/icons/magnifyingglass.jsx | 23 ++ src/ui/publicservers/card.jsx | 54 ++-- src/ui/publicservers/menu.js | 130 ++++++---- src/ui/publicservers/noresults.jsx | 11 + src/ui/settings/components/search.jsx | 2 +- 9 files changed, 362 insertions(+), 223 deletions(-) create mode 100644 src/ui/icons/magnifyingglass.jsx create mode 100644 src/ui/publicservers/noresults.jsx diff --git a/src/data/changelog.js b/src/data/changelog.js index b8fd5487..2be36d89 100644 --- a/src/data/changelog.js +++ b/src/data/changelog.js @@ -11,7 +11,8 @@ export default { "**Floating editors** for both custom css and plugins/themes are now available. (See video above)", "**Settings panels** are completely new and sleek. They are also highly extensible for potential future features :eyes:", "**Translations** are now integrated starting with only a couple languages, but feel free to contribute your own!", - "**Emote menu** now uses React Patching and properly integrates into the new Emoji Picker. (Thanks Strencher#1044!)" + "**Emote menu** now uses React Patching and properly integrates into the new Emoji Picker. (Thanks Strencher#1044!)", + "**Public servers** got a new makeover thanks to some design help from Tropical and Gibbu!" ] }, { diff --git a/src/data/strings.js b/src/data/strings.js index b1b52254..81a350fc 100644 --- a/src/data/strings.js +++ b/src/data/strings.js @@ -251,11 +251,13 @@ export default { joined: "Joined", loading: "Loading", loadMore: "Load More", - notConnected: "Not connected to DiscordServers.com!", + notConnected: "Not Connected", + connectionRequired: "You must connect your account in order to join servers.", search: "Search", connect: "Connect", reconnect: "Reconnect", categories: "Categories", + keywords: "Keywords", connection: "Connected as: {{username}}#{{discriminator}}", results: "Showing {{start}}-{{end}} of {{total}} results in {{category}}", query: "for {{query}}" @@ -264,6 +266,7 @@ export default { confirmAction: "Are You Sure?", okay: "Okay", cancel: "Cancel", + nevermind: "Nevermind", close: "Close", name: "Name", message: "Message", diff --git a/src/structs/psconnection.js b/src/structs/psconnection.js index 8dea3aa2..91b2350c 100644 --- a/src/structs/psconnection.js +++ b/src/structs/psconnection.js @@ -21,11 +21,11 @@ export default class PublicServersConnection { return SortedGuildStore.getFlattenedGuildIds().includes(id); } - static search({term = "", category = "", from = 0} = {}) { + static search({term = "", keyword = "", from = 0} = {}) { const request = require("request"); return new Promise(resolve => { const queries = []; - if (category) queries.push(`category=${category.replace(/ /g, "%20")}`); + if (keyword) queries.push(`keyword=${keyword.replace(/ /g, "%20").toLowerCase()}`); if (term) queries.push(`term=${term.replace(/ /g, "%20")}`); if (from) queries.push(`from=${from}`); const query = `?${queries.join("&")}`; @@ -64,7 +64,7 @@ export default class PublicServersConnection { static async checkConnection() { try { - const response = await fetch(`https://auth.discordservers.com/info`,{ + const response = await fetch(this.connectEndPoint, { method: "GET", credentials: "include", mode: "cors", @@ -82,6 +82,19 @@ export default class PublicServersConnection { } } + static async getDashboard() { + try { + const response = await fetch(`${this.endPoint}/dashboard`, { + method: "GET" + }); + const data = await response.json(); + return data; + } + catch (error) { + return false; + } + } + static connect() { return new Promise(resolve => { const joinWindow = new BrowserWindow(this.windowOptions); diff --git a/src/styles/builtins/publicservers.css b/src/styles/builtins/publicservers.css index ec15107c..0ae19020 100644 --- a/src/styles/builtins/publicservers.css +++ b/src/styles/builtins/publicservers.css @@ -14,107 +14,6 @@ height: 20px; } -.bd-server-card .bd-server-tags { - flex: 1 1 auto; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - width: 0; - line-height: 24px; - font-size: 12px; - color: #b9bbbe; - font-weight: 700; - margin-right: 10px; -} - -/* .ui-card.ui-card-primary.bd-server-card:first-child { - margin-bottom: 13px; -} - -.ui-card.ui-card-primary.bd-server-card:first-child:after { - border: 3px solid #7289da; - content: ""; - display: block; - position: absolute; - left: 0; - right: 0; - margin-top: 4px; -} */ - -.bd-server-card.bd-server-card-pinned { - margin-bottom: 15px; -} - -.bd-server-card.bd-server-card-pinned::after { - background: #3a71c1; - content: ""; - height: 3px; - width: 100%; - display: block; - margin-top: 7px; - position: absolute; - top: 100%; -} - -.bd-server-description-container { - color: #b9bbbe; - min-height: 65px; - max-height: 65px; - border-top: 1px solid #3f4146; - border-bottom: 1px solid #3f4146; - padding-top: 5px; - font-size: 13px; -} - -.bd-server-header { - text-transform: uppercase; - letter-spacing: 0.5px; - justify-content: space-between; - font-weight: 600; -} - -.bd-server-card { - display: flex; - position: relative; - border-width: 1px; - border-style: solid; - border-radius: 5px; - background: rgba(32, 34, 37, 0.6); - border-color: #202225; - margin-bottom: 8px; -} - -.bd-server-header, -.bd-server-footer { - display: flex; - color: #b9bbbe; -} - -.bd-server-card .bd-button { - margin-top: 4px; -} - -.bd-server-content { - padding: 5px 10px; - flex: 1; -} - -.bd-server-image { - min-width: 115px; - min-height: 115px; - max-width: 115px; - max-height: 115px; -} - -.bd-server-name { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - padding-right: 15px; - max-width: 330px; - flex: 1 1 50%; -} - .bd-layer { -ms-flex-direction: column; -webkit-box-direction: normal; @@ -130,66 +29,6 @@ top: 0; } -/* #pubslayer .ui-tab-bar-item { - color: #b9bbbe; - padding-top: 6px; - padding-bottom: 6px; - margin-bottom: 2px; - padding: 6px 10px; - position: relative; - font-size: 16px; - line-height: 20px; - border-radius: 3px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - flex-shrink: 0; - font-weight: 500; - cursor: pointer; -} - -#pubslayer .ui-tab-bar-item:hover { - color: #f6f6f7; - background-color: hsla(216,4%,74%,.1); -} - -#pubslayer .ui-tab-bar-item.selected { - color: #fff; - background-color: #7289da; -} - -#pubslayer .ui-tab-bar-header { - color: #72767d; - padding: 6px 10px; - font-size: 12px; - line-height: 16px; - text-transform: uppercase; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - flex-shrink: 0; - font-weight: 500; -} - -#pubslayer #bd-settings-sidebar .ui-tab-bar-separator { - background-color: hsla(218,5%,47%,.3); - margin-left: 10px; - margin-right: 10px; - height: 1px; - margin-bottom: 8px; - margin-top: 8px; -} - -#pubslayer h2.ui-form-title { - color: #f6f6f7; - text-transform: uppercase; - font-weight: 600; -} - -#pubslayer h5.ui-form-title { - color: #f6f6f7; -} */ - #pubslayer button { background: #7289da; color: #fff; @@ -233,4 +72,181 @@ margin: 5px 10px 10px 0; width: 100%; min-height: 20px; +} + +/* Rewrite */ +.bd-server-search { + margin-bottom: 5px; +} + +.bd-empty-results { + display: flex; + flex-direction: column; + align-items: center; + color: var(--text-normal); + margin-top: 100px; +} + +.bd-empty-results svg { + margin-bottom: 20px; +} + +.bd-card-list { + display: grid; + grid-gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); +} + +.bd-server-card { + display: flex; + flex-direction: column; + height: 320px; + width: 100%; + overflow: hidden; + border-radius: 8px; + position: relative; + transition: box-shadow 0.2s ease-out, transform 0.2s ease-out, background 0.2s ease-out, opacity 0.2s ease-in; + cursor: pointer; + background-color: var(--background-secondary-alt); +} + +.bd-server-card:hover { + background-color: var(--background-tertiary); + transform: translateY(-1px); + box-shadow: var(--elevation-high); +} + +.bd-server-header { + height: 143px; + position: relative; + display: block; + overflow: visible; + margin-bottom: 32px; +} + +.bd-server-splash-container { + width: 100%; + height: 100%; + display: block; + position: absolute; + top: 0; + left: 0; + transition: opacity 0.2s, transform 0.2s ease-out; + transform: scale(1); + overflow: hidden; +} + +.bd-server-card:hover .bd-server-splash-container { + -webkit-transform: scale(1.01) translateZ(0); + transform: scale(1.01) translateZ(0); +} + +.bd-server-splash { + object-fit: cover; + width: 100%; + height: 100%; + filter: blur(20px); +} + +.bd-server-icon { + position: absolute; + bottom: -21px; + left: 12px; + width: 40px; + background: var(--background-secondary-alt); + border: 4px solid var(--background-secondary-alt); + border-radius: 25%; + transition: background 0.2s ease-out, transform 0.2s ease-out, border-color 0.2s ease-out; +} + +.bd-server-card:hover .bd-server-icon { + border-color: var(--background-tertiary); + background: var(--background-tertiary); +} + +.bd-server-info { + display: flex; + flex: 1 1 auto; + position: relative; + flex-direction: column; + align-content: stretch; + padding: 0 16px 16px; + overflow: hidden; +} + +.bd-server-title { + display: flex; + align-items: center; + width: 100%; + font-weight: 600; +} + +.bd-server-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--header-primary); + font-size: 16px; + line-height: 20px; +} + +.bd-server-description { + flex: 1 1 auto; + overflow: hidden; + margin: 4px 0 16px; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + color: var(--header-secondary); + font-size: 14px; + line-height: 18px; +} + +.bd-server-footer { + display: flex; + align-items: center; +} + +.bd-server-count { + display: flex; + align-items: center; + font-size: 0.75rem; + line-height: 1rem; + margin-right: 16px; +} + +.bd-server-count-dot { + background-color: #43b581; + border-radius: 50%; + width: 8px; + height: 8px; + margin-right: 4px; + flex-shrink: 0; +} + +.bd-server-count + .bd-server-count .bd-server-count-dot { + background-color: #b9bbbe; +} + +.bd-server-count-text { + color: var(--header-secondary); + font-size: 12px; + line-height: 16px; +} + +.bd-server-tag { + margin-left: 5px; + font-size: 10px; + text-transform: uppercase; + vertical-align: top; + display: inline-flex; + align-items: center; + flex-shrink: 0; + text-indent: 0; + height: 15px; + padding: 0 4px; + margin-top: 1px; + border-radius: 3px; + background: #3e82e5; + color: #fff; } \ No newline at end of file diff --git a/src/ui/icons/magnifyingglass.jsx b/src/ui/icons/magnifyingglass.jsx new file mode 100644 index 00000000..1b83e7d1 --- /dev/null +++ b/src/ui/icons/magnifyingglass.jsx @@ -0,0 +1,23 @@ +import {React} from "modules"; + +export default class MagnifyingGlass extends React.Component { + render() { + const size = this.props.size || "160px"; + return + + + + + + + + + + + + + + + ; + } +} \ No newline at end of file diff --git a/src/ui/publicservers/card.jsx b/src/ui/publicservers/card.jsx index 37429247..e592f02f 100644 --- a/src/ui/publicservers/card.jsx +++ b/src/ui/publicservers/card.jsx @@ -1,5 +1,20 @@ import {React, Strings} from "modules"; +const badge =
+ + + + +
+ + + +
+
; + export default class ServerCard extends React.Component { constructor(props) { super(props); @@ -14,24 +29,33 @@ export default class ServerCard extends React.Component { render() { const {server} = this.props; + const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp const buttonText = typeof(this.state.joined) == "string" ? `${Strings.PublicServers.joining}...` : this.state.joined ? Strings.PublicServers.joined : Strings.PublicServers.join; - const buttonClass = `bd-button${this.state.joined == true ? " bd-button-success" : ""}`; - return
- , -
+ + return
-
{server.name}
-
{server.members} Members
-
-
-
{server.description}
+
+
-
-
{server.categories.join(", ")}
- +
+
+ {server.pinned && badge} +
{server.name}
+ {this.state.joined &&
{buttonText}
}
-
-
; +
{server.description}
+
+
+
+
{server.members.toLocaleString()} Members
+
+
+
+
Added {addedDate.toLocaleDateString()}
+
+
+
+
; } handleError() { @@ -40,7 +64,7 @@ export default class ServerCard extends React.Component { } async join() { - if (this.state.joined) return; + if (this.state.joined) return this.props.navigateTo(this.props.server.identifier); this.setState({joined: "joining"}); const didJoin = await this.props.join(this.props.server.identifier, this.props.server.nativejoin); this.setState({joined: didJoin}); diff --git a/src/ui/publicservers/menu.js b/src/ui/publicservers/menu.js index 16518f9f..80ff1f50 100644 --- a/src/ui/publicservers/menu.js +++ b/src/ui/publicservers/menu.js @@ -1,21 +1,35 @@ import {React, WebpackModules, Strings} from "modules"; +import Modals from "../modals"; import SettingsTitle from "../settings/title"; import ServerCard from "./card"; +import EmptyResults from "./noresults"; import Connection from "../../structs/psconnection"; import Search from "../settings/components/search"; + const SettingsView = WebpackModules.getByDisplayName("SettingsView"); +const GuildActions = WebpackModules.getByProps("transitionToGuildSync"); +const LayerManager = WebpackModules.getByProps("popLayer"); + +const betterDiscordServer = { + name: "BetterDiscord", + members: 55000, + categories: ["community", "programming", "support"], + description: "Official BetterDiscord server for plugins, themes, support, etc", + identifier: "86004744966914048", + iconUrl: "https://cdn.discordapp.com/icons/86004744966914048/292e7f6bfff2b71dfd13e508a859aedd.webp", + nativejoin: true, + invite_code: "BJD2yvJ", + pinned: true, + insertDate: 1517806800 +}; export default class PublicServers extends React.Component { - get categoryButtons() { - return ["All", "FPS Games", "MMO Games", "Strategy Games", "MOBA Games", "RPG Games", "Tabletop Games", "Sandbox Games", "Simulation Games", "Music", "Community", "Language", "Programming", "Other"]; - } - constructor(props) { super(props); this.state = { - category: "All", + tab: "Featured", query: "", loading: true, user: null, @@ -28,24 +42,44 @@ export default class PublicServers extends React.Component { } }; - this.changeCategory = this.changeCategory.bind(this); + this.featured = []; + this.popular = []; + this.keywords = []; + + this.changeTab = this.changeTab.bind(this); this.searchKeyDown = this.searchKeyDown.bind(this); this.connect = this.connect.bind(this); this.loadNextPage = this.loadNextPage.bind(this); this.join = this.join.bind(this); + this.navigateTo = this.navigateTo.bind(this); } componentDidMount() { + this.getDashboard(); this.checkConnection(); } async checkConnection() { const userData = await Connection.checkConnection(); - if (!userData) { - return this.setState({loading: true, user: null}); - } + if (!userData) return this.setState({user: null}); this.setState({user: userData}); - this.search(); + } + + async getDashboard() { + const dashboardData = await Connection.getDashboard(); + const featuredFirst = dashboardData.results[0].key === "featured"; + const featuredServers = dashboardData.results[featuredFirst ? 0 : 1].response.hits; + const popularServers = dashboardData.results[featuredFirst ? 1 : 0].response.hits; + const mainKeywords = dashboardData.mainKeywords.map(k => k.charAt(0).toUpperCase() + k.slice(1)).sort(); + + featuredServers.unshift(betterDiscordServer); + + this.featured = featuredServers; + this.popular = popularServers; + this.keywords = mainKeywords; + + this.setState({loading: false}); + this.changeTab(this.state.tab); } async connect() { @@ -55,12 +89,14 @@ export default class PublicServers extends React.Component { searchKeyDown(e) { if (this.state.loading || e.which !== 13) return; - this.search(e.target.value); + const term = e.target.value; + if (this.state.tab == "Featured" || this.state.tab == "Popular") this.setState({tab: "All"}, () => this.search(term)); + else this.search(term); } async search(term = "", from = 0) { this.setState({query: term, loading: true}); - const results = await Connection.search({term, category: this.state.category == "All" ? "" : this.state.category, from}); + const results = await Connection.search({term, keyword: this.state.tab == "All" || this.state.tab == "Featured" || this.state.tab == "Popular" ? "" : this.state.tab, from}); if (!results) { return this.setState({results: { servers: [], @@ -70,12 +106,23 @@ export default class PublicServers extends React.Component { next: null }}); } + this.setState({loading: false, results}); } - async changeCategory(id) { + async changeTab(id) { if (this.state.loading) return; - await new Promise(resolve => this.setState({category: id}, resolve)); + await new Promise(resolve => this.setState({tab: id}, resolve)); + if (this.state.tab === "Featured" || this.state.tab == "Popular") { + return this.setState({results: { + servers: this[this.state.tab.toLowerCase()], + size: this[this.state.tab.toLowerCase()].length, + from: 0, + total: this[this.state.tab.toLowerCase()].length, + next: null + }}); + } + this.search(); } @@ -85,33 +132,44 @@ export default class PublicServers extends React.Component { } async join(id, native = false) { + if (!this.state.user && !native) { + return Modals.showConfirmationModal(Strings.PublicServers.notConnected, Strings.PublicServers.connectionRequired, { + cancelText: Strings.Modals.nevermind, + confirmText: Strings.Modals.okay, + onConfirm: () => { + this.connect().then(() => Connection.join(id, native)); + } + }); + } return await Connection.join(id, native); } + navigateTo(id) { + if (GuildActions) GuildActions.transitionToGuildSync(id); + if (LayerManager) LayerManager.popLayer(); + } + get searchBox() { - return ; + return ; } get title() { - if (!this.state.user) return Strings.PublicServers.notConnected; if (this.state.loading) return `${Strings.PublicServers.loading}...`; const start = this.state.results.from + 1; const total = this.state.results.total; const end = this.state.results.next ? this.state.results.next : total; - let title = Strings.PublicServers.results.format({start, end, total, category: this.state.category}); + let title = Strings.PublicServers.results.format({start, end, total, category: this.state.tab}); if (this.state.query) title += " " + Strings.PublicServers.query.format({query: this.state.query}); return title; } get content() { const connectButton = this.state.user ? null : {title: Strings.PublicServers.connect, onClick: this.connect}; - const pinned = this.state.category == "All" || !this.state.user ? this.bdServer : null; const servers = this.state.results.servers.map((server) => { - return React.createElement(ServerCard, {key: server.identifier, server: server, joined: Connection.hasJoined(server.identifier), join: this.join, defaultAvatar: Connection.getDefaultAvatar}); + return React.createElement(ServerCard, {key: server.identifier, server: server, joined: Connection.hasJoined(server.identifier), join: this.join, navigateTo: this.navigateTo, defaultAvatar: Connection.getDefaultAvatar}); }); return [React.createElement(SettingsTitle, {text: this.title, button: connectButton}), - pinned, - servers, + this.state.results.total ? React.createElement("div", {className: "bd-card-list"}, servers) : React.createElement(EmptyResults), this.state.results.next ? this.nextButton : null, this.state.results.servers.length > 0 && React.createElement(SettingsTitle, {text: this.title})]; } @@ -129,24 +187,8 @@ export default class PublicServers extends React.Component { ); } - get bdServer() { - const server = { - name: "BetterDiscord", - online: "7500+", - members: "20000+", - categories: ["community", "programming", "support"], - description: "Official BetterDiscord server for plugins, themes, support, etc", - identifier: "86004744966914048", - iconUrl: "https://cdn.discordapp.com/icons/86004744966914048/292e7f6bfff2b71dfd13e508a859aedd.webp", - nativejoin: true, - invite_code: "0Tmfo5ZbORCRqbAd", - pinned: true - }; - return React.createElement(ServerCard, {server: server, pinned: true, joined: Connection.hasJoined(server.identifier), defaultAvatar: Connection.getDefaultAvatar}); - } - render() { - const categories = this.categoryButtons.map(name => ({ + const keywords = this.keywords.map(name => ({ section: name, label: name, element: () => this.content @@ -154,13 +196,19 @@ export default class PublicServers extends React.Component { ); return React.createElement(SettingsView, { onClose: this.props.close, - onSetSection: this.changeCategory, - section: this.state.category, + onSetSection: this.changeTab, + section: this.state.tab, sections: [ {section: "HEADER", label: Strings.PublicServers.search}, {section: "CUSTOM", element: () => this.searchBox}, + {section: "DIVIDER"}, {section: "HEADER", label: Strings.PublicServers.categories}, - ...categories, + {section: "All", label: "All", element: () => this.content}, + {section: "Featured", label: "Featured", element: () => this.content}, + {section: "Popular", label: "Popular", element: () => this.content}, + {section: "DIVIDER"}, + {section: "HEADER", label: Strings.PublicServers.keywords}, + ...keywords, {section: "DIVIDER"}, {section: "HEADER", label: React.createElement("a", {href: "https://discordservers.com", target: "_blank"}, "DiscordServers.com")}, {section: "DIVIDER"}, diff --git a/src/ui/publicservers/noresults.jsx b/src/ui/publicservers/noresults.jsx new file mode 100644 index 00000000..6b58aef5 --- /dev/null +++ b/src/ui/publicservers/noresults.jsx @@ -0,0 +1,11 @@ +import {React, DiscordModules} from "modules"; +import MagnifyingGlass from "../icons/magnifyingglass"; + +export default class NoResults extends React.Component { + render() { + return
+ + {DiscordModules.Strings.SEARCH_NO_RESULTS || ""} +
; + } +} \ No newline at end of file diff --git a/src/ui/settings/components/search.jsx b/src/ui/settings/components/search.jsx index e7e5e8b3..21686612 100644 --- a/src/ui/settings/components/search.jsx +++ b/src/ui/settings/components/search.jsx @@ -3,7 +3,7 @@ import SearchIcon from "../../icons/search"; export default class Search extends React.Component { render() { - return
+ return
;