diff --git a/sticker/lib/matrix.py b/sticker/lib/matrix.py index e506e83..b242d37 100644 --- a/sticker/lib/matrix.py +++ b/sticker/lib/matrix.py @@ -55,10 +55,18 @@ async def load_config(path: str) -> None: config = json.load(config_file) homeserver_url = config["homeserver"] access_token = config["access_token"] + try: + giphy_api_key = config["giphy_api_key"] + giphy_mxc_prefix = config["giphy_mxc_prefix"] + except KeyError: + # these two are not mandatory, assume GIF search is disabled + print("Giphy related parameters not found in the config file.") except FileNotFoundError: - print("Matrix config file not found. Please enter your homeserver and access token.") + print("Matrix config file not found. Please enter your homeserver and access token. Enter the Giphy API token if required, leave blank to disable the gif picker.") homeserver_url = input("Homeserver URL: ") access_token = input("Access token: ") + giphy_api_key = input("Giphy API key: ") + giphy_mxc_prefix = input("Giphy MXC prefix. Defaults to 'mxc://giphy.mau.dev/', required to proxy GIFs: ") whoami_url = URL(homeserver_url) / "_matrix" / "client" / "r0" / "account" / "whoami" if whoami_url.scheme not in ("https", "http"): whoami_url = whoami_url.with_scheme("https") @@ -67,7 +75,9 @@ async def load_config(path: str) -> None: json.dump({ "homeserver": homeserver_url, "user_id": user_id, - "access_token": access_token + "access_token": access_token, + "giphy_api_key": giphy_api_key, + "giphy_mxc_prefix": giphy_mxc_prefix, }, config_file) print(f"Wrote config to {path}") diff --git a/web/index.html b/web/index.html index 6312814..78e5170 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,6 @@ Maunium sticker picker - diff --git a/web/src/index.js b/web/src/index.js index a215c46..fb5b61b 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -18,6 +18,7 @@ import { Spinner } from "./spinner.js" import { SearchBox } from "./search-box.js" import * as widgetAPI from "./widget-api.js" import * as frequent from "./frequently-used.js" +// import GiphyAPI from "./GiphySearch.js" // The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json, // then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file. @@ -30,6 +31,8 @@ if (params.has('config')) { } // This is updated from packs/index.json let HOMESERVER_URL = "https://matrix-client.matrix.org" +let GIPHY_API_KEY = "" +let GIPHY_MXC_PREFIX = "mxc://giphy.mau.dev/" const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale` @@ -39,6 +42,7 @@ const isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && naviga const supportedThemes = ["light", "dark", "black"] + const defaultState = { packs: [], filtering: { @@ -47,11 +51,101 @@ const defaultState = { }, } +class GiphySearchTab extends Component { + constructor(props) { + super(props); + this.state = { + searchTerm: "", + gifs: [], + loading: false, + GIFById: {}, + }; + this.handleSearchChange = this.handleSearchChange.bind(this); + this.searchGifs = this.searchGifs.bind(this); + this.handleGifClick = this.handleGifClick.bind(this); + } + + async searchGifs() { + this.setState({ loading: true }); + try { + const apiKey = GIPHY_API_KEY; + const mxc_prefix = GIPHY_MXC_PREFIX; + const url = `https://api.giphy.com/v1/gifs/search?q=${this.state.searchTerm}&api_key=${apiKey}`; + this.setState({ loading: true }); + const response = await fetch(url); + const data = await response.json(); + this.setState({ gifs: data.data, loading: false }); + data.data.forEach((jsonElement) => { + const id = jsonElement.id; + const updatedItem = { + "body": jsonElement.title, + "info": { + "h": jsonElement.images.original.height, + "w": jsonElement.images.original.width, + "size": jsonElement.images.original.size, + "mimetype": "image/webp", + }, + "msgtype": "m.image", + "url": mxc_prefix+jsonElement.id + }; + this.setState((prevState) => ({ + GIFById: {...prevState.GIFById, [id]: updatedItem}})); + }); + } catch (error) { + this.setState({ error: "Error fetching GIFs", loading: false }); + this.setState({ loading: false }); + } + } + + handleSearchChange(event) { + this.setState({ searchTerm: event.target.value }); + } + + handleGifClick(gif) { + console.log(this.state.GIFById[gif.id]); + widgetAPI.sendGIF(this.state.GIFById[gif.id]); + } + async searchGiphy(searchTerm) { + if (!searchTerm) return; + +}; + + render() { + const { searchTerm, gifs, loading } = this.state; + + return html` + + +
+
+
+ ${GIPHY_API_KEY !== "" && gifs.map((gif) => html` +
this.handleGifClick(gif)} data-gif-id=${gif.id}> + ${gif.title} +
+ `)} +
+
+ + `; + } +} + + class App extends Component { constructor(props) { super(props) this.defaultTheme = params.get("theme") this.state = { + activeTab: "stickers", packs: defaultState.packs, loading: true, error: null, @@ -164,6 +258,8 @@ class App extends Component { } const indexData = await indexRes.json() HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL + GIPHY_API_KEY = indexData.giphy_api_key || "" + GIPHY_MXC_PREFIX = indexData.giphy_mxc_prefix || GIPHY_MXC_PREFIX // TODO only load pack metadata when scrolled into view? for (const packFile of indexData.packs) { let packRes @@ -266,35 +362,54 @@ class App extends Component { } render() { - const theme = `theme-${this.state.theme}` - const filterActive = !!this.state.filtering.searchTerm - const packs = filterActive ? this.state.filtering.packs : [this.state.frequentlyUsed, ...this.state.packs] + const theme = `theme-${this.state.theme}`; + const filterActive = !!this.state.filtering.searchTerm; + const packs = filterActive + ? this.state.filtering.packs + : [this.state.frequentlyUsed, ...this.state.packs]; - if (this.state.loading) { - return html`
<${Spinner} size=${80} green />
` - } else if (this.state.error) { - return html`
-

Failed to load packs

-

${this.state.error}

-
` - } else if (this.state.packs.length === 0) { - return html`

No packs found 😿

` - } + if (this.state.loading) { + return html`
<${Spinner} size=${80} green />
`; + } else if (this.state.error) { + return html`
+

Failed to load packs

+

${this.state.error}

+
`; + } else if (this.state.packs.length === 0) { + return html`

No packs found 😿

`; + } - return html`
- - <${SearchBox} onKeyUp=${this.searchStickers} /> -
this.packListRef = elem}> - ${filterActive && packs.length === 0 ? html`

No stickers match your search

` : null} - ${packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)} - <${Settings} app=${this}/> -
-
` - } + return html`
+
+ this.setState({ activeTab: "stickers" })}>Stickers + this.setState({ activeTab: "gifs" })}>GIFs +
+ + ${this.state.activeTab === "stickers" && html` + + + <${SearchBox} onKeyUp=${this.searchStickers} /> +
(this.packListRef = elem)}> + ${filterActive && packs.length === 0 + ? html`

No stickers match your search

` + : null} + ${packs.map((pack) => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)} + <${Settings} app=${this} /> +
+ + `} + ${this.state.activeTab === "gifs" && GIPHY_API_KEY !== "" && html` + <${GiphySearchTab} send=${this.sendGIF} /> + `} + ${this.state.activeTab === "gifs" && GIPHY_API_KEY === "" && html` +

GIF Search is not enabled. Please enable it in the config.

+ `} +
`; + } } const Settings = ({ app }) => html` diff --git a/web/src/widget-api.js b/web/src/widget-api.js index fa72165..a078efd 100644 --- a/web/src/widget-api.js +++ b/web/src/widget-api.js @@ -74,3 +74,7 @@ export function sendSticker(content) { widgetData, }, "*") } + +export function sendGIF(content) { + return sendSticker(content) +} diff --git a/web/style/index.css b/web/style/index.css index e33e450..de8db30 100644 --- a/web/style/index.css +++ b/web/style/index.css @@ -1 +1 @@ -*{font-family:sans-serif}body{margin:0}h1{font-size:1rem}:root{--stickers-per-row: 4;--sticker-size: calc(100vw / var(--stickers-per-row))}main{color:var(--text-color)}main.spinner{margin-top:5rem}main.error,main.empty{margin:2rem}main.empty{text-align:center}main.has-content{position:fixed;top:0;left:0;right:0;bottom:0;display:grid;grid-template-rows:calc(12vw + 2px) min-content auto}main.theme-light{--highlight-color: #eee;--search-box-color: var(--highlight-color);--text-color: black;background-color:#fff}main.theme-dark{--highlight-color: #444;--search-box-color: #383e4b;--text-color: white;background-color:#22262e}main.theme-black{--highlight-color: #222;--search-box-color: var(--highlight-color);--text-color: white;background-color:#000}.icon{width:100%;height:100%;background-color:var(--text-color);mask-size:contain;-webkit-mask-size:contain;mask-image:var(--icon-image);-webkit-mask-image:var(--icon-image)}.icon.icon-settings{--icon-image: url(../res/settings.svg)}.icon.icon-recent{--icon-image: url(../res/recent.svg)}.icon.icon.icon-search{--icon-image: url(../res/search.svg)}nav{display:flex;overflow-x:auto}nav>a{border-bottom:2px solid transparent}nav>a.visible{border-bottom-color:green}nav>a>div.sticker{width:12vw;height:12vw}div.pack-list,nav{scrollbar-width:none}div.pack-list::-webkit-scrollbar,nav::-webkit-scrollbar{display:none}div.pack-list{overflow-y:auto}div.pack-list.ios-safari-hack{position:fixed;top:calc(calc(12vw + 2px) + calc(2 * 0.7rem + 2 * 0.5rem + 1rem));bottom:0;left:0;right:0;-webkit-overflow-scrolling:touch}div.search-empty{margin:1.2rem;text-align:center}section.stickerpack{margin-top:.75rem}section.stickerpack>div.sticker-list{display:flex;flex-wrap:wrap}section.stickerpack>h1{margin:0 0 0 .75rem}div.sticker{display:flex;padding:4px;cursor:pointer;position:relative;width:var(--sticker-size);height:var(--sticker-size);box-sizing:border-box}div.sticker:hover{background-color:var(--highlight-color)}div.sticker>img{display:none;width:100%;object-fit:contain}div.sticker>img.visible{display:initial}div.sticker>.icon{width:70%;height:70%;margin:15%}div.search-box{position:relative;display:flex}div.search-box>input[type=text]{flex-grow:1;background-color:var(--search-box-color);outline:none;border:none;border-radius:.25rem;height:1rem;padding:.7rem;padding-right:calc(1rem + 0.7rem);margin:.5rem;font-size:1rem;color:var(--text-color)}div.search-box>span.icon{display:flex;position:absolute;top:calc(50% - 1rem / 2);right:1rem;width:1rem;height:1rem;box-sizing:border-box}div.settings-list{display:flex;flex-direction:column}div.settings-list>*{margin:.5rem}div.settings-list button{padding:.5rem;border-radius:.25rem}div.settings-list input{width:100%} +*{font-family:sans-serif}body{margin:0}h1{font-size:1rem}:root{--stickers-per-row: 4;--sticker-size: calc(100vw / var(--stickers-per-row))}main{color:var(--text-color)}main.spinner{margin-top:5rem}main.error,main.empty{margin:2rem}main.empty{text-align:center}main.has-content{position:fixed;top:0;left:0;right:0;bottom:0;display:grid;grid-template-rows:calc(12vw + 2px) min-content auto}main.theme-light{--highlight-color: #eee;--search-box-color: var(--highlight-color);--text-color: black;background-color:#fff}main.theme-dark{--highlight-color: #444;--search-box-color: #383e4b;--text-color: white;background-color:#22262e}main.theme-black{--highlight-color: #222;--search-box-color: var(--highlight-color);--text-color: white;background-color:#000}.icon{width:100%;height:100%;background-color:var(--text-color);mask-size:contain;-webkit-mask-size:contain;mask-image:var(--icon-image);-webkit-mask-image:var(--icon-image)}.icon.icon-settings{--icon-image: url(../res/settings.svg)}.icon.icon-recent{--icon-image: url(../res/recent.svg)}.icon.icon.icon-search{--icon-image: url(../res/search.svg)}nav{display:flex;overflow-x:auto}nav>a{border-bottom:2px solid rgba(0,0,0,0)}nav>a.visible{border-bottom-color:green}nav>a>div.sticker{width:12vw;height:12vw}div.pack-list,nav{scrollbar-width:none}div.pack-list::-webkit-scrollbar,nav::-webkit-scrollbar{display:none}div.pack-list{overflow-y:auto}div.pack-list.ios-safari-hack{position:fixed;top:calc(calc(12vw + 2px) + calc(2 * 0.7rem + 2 * 0.5rem + 1rem));bottom:0;left:0;right:0;-webkit-overflow-scrolling:touch}div.search-empty{margin:1.2rem;text-align:center}section.stickerpack{margin-top:.75rem}section.stickerpack>div.sticker-list{display:flex;flex-wrap:wrap}section.stickerpack>h1{margin:0 0 0 .75rem}div.sticker{display:flex;padding:4px;cursor:pointer;position:relative;width:var(--sticker-size);height:var(--sticker-size);box-sizing:border-box}div.sticker:hover{background-color:var(--highlight-color)}div.sticker>img{display:none;width:100%;object-fit:contain}div.sticker>img.visible{display:initial}div.sticker>.icon{width:70%;height:70%;margin:15%}div.search-box{position:relative;display:flex}div.search-box>input[type=text]{flex-grow:1;background-color:var(--search-box-color);outline:none;border:none;border-radius:.25rem;height:1rem;padding:.7rem;padding-right:calc(1rem + 0.7rem);margin:.5rem;font-size:1rem;color:var(--text-color)}div.search-box>span.icon{display:flex;position:absolute;top:calc(50% - 1rem/2);right:1rem;width:1rem;height:1rem;box-sizing:border-box}div.settings-list{display:flex;flex-direction:column}div.settings-list>*{margin:.5rem}div.settings-list button{padding:.5rem;border-radius:.25rem}div.settings-list input{width:100%}a.tab{padding:5% 5%;width:40%;text-align:center;border:none;background-color:#f0f0f0;cursor:pointer;-webkit-appearance:button;-moz-appearance:button;appearance:button;text-decoration:none;color:initial} diff --git a/web/style/index.sass b/web/style/index.sass index c079f65..bc8c059 100644 --- a/web/style/index.sass +++ b/web/style/index.sass @@ -204,3 +204,16 @@ div.settings-list input width: 100% +a.tab + padding: 5% 5% + width: 40% + text-align: center + border: none + background-color: #f0f0f0 + cursor: pointer + -webkit-appearance: button + -moz-appearance: button + appearance: button + + text-decoration: none + color: initial