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
;