Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Giphy GIF search integration #75

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions sticker/lib/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
tulir marked this conversation as resolved.
Show resolved Hide resolved
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")
Expand All @@ -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}")

Expand Down
1 change: 0 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
<title>Maunium sticker picker</title>

<link rel="modulepreload" href="src/widget-api.js"/>
<link rel="modulepreload" href="src/frequently-used.js"/>
<link rel="modulepreload" href="src/spinner.js"/>
Expand Down
173 changes: 144 additions & 29 deletions web/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`

Expand All @@ -39,6 +42,7 @@ const isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && naviga

const supportedThemes = ["light", "dark", "black"]


const defaultState = {
packs: [],
filtering: {
Expand All @@ -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`
<div class="search-box">
<input
type="text"
value=${searchTerm}
onInput=${this.handleSearchChange}
placeholder="Search GIFs..."
/>
<button onClick=${this.searchGifs} disabled=${loading}>Search</button>
</div>
<!-- <div class="gifs-list" style="display: grid"> -->
<div class="pack-list">
<section class="stickerpack">
<div class="sticker-list">
${GIPHY_API_KEY !== "" && gifs.map((gif) => html`
<div class="sticker" onClick=${() => this.handleGifClick(gif)} data-gif-id=${gif.id}>
<img src=${gif.images.fixed_height.url} alt=${gif.title} class="visible" data=/>
</div>
`)}
</div>
</div>
</div>
`;
}
}


class App extends Component {
constructor(props) {
super(props)
this.defaultTheme = params.get("theme")
this.state = {
activeTab: "stickers",
packs: defaultState.packs,
loading: true,
error: null,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]

if (this.state.loading) {
return html`<main class="spinner ${theme}"><${Spinner} size=${80} green /></main>`
} else if (this.state.error) {
return html`<main class="error ${theme}">
<h1>Failed to load packs</h1>
<p>${this.state.error}</p>
</main>`
} else if (this.state.packs.length === 0) {
return html`<main class="empty ${theme}"><h1>No packs found 😿</h1></main>`
}

return html`<main class="has-content ${theme}">
<nav onWheel=${this.navScroll} ref=${elem => this.navRef = elem}>
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="recent" />
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
<${NavBarItem} pack=${{ id: "settings", title: "Settings" }} iconOverride="settings" />
</nav>
<${SearchBox} onKeyUp=${this.searchStickers} />
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${elem => this.packListRef = elem}>
${filterActive && packs.length === 0 ? html`<div class="search-empty"><h1>No stickers match your search</h1></div>` : null}
${packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)}
<${Settings} app=${this}/>
</div>
</main>`
}
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`<main class="spinner ${theme}"><${Spinner} size=${80} green /></main>`;
} else if (this.state.error) {
return html`<main class="error ${theme}">
<h1>Failed to load packs</h1>
<p>${this.state.error}</p>
</main>`;
} else if (this.state.packs.length === 0) {
return html`<main class="empty ${theme}"><h1>No packs found 😿</h1></main>`;
}

return html`<main class="has-content ${theme}">
<div class="tab-container" style="display: flex;">
<a href="#stickers" class="tab" onClick=${() => this.setState({ activeTab: "stickers" })}>Stickers</a>
<a href="#gifs" class="tab" onClick=${() => this.setState({ activeTab: "gifs" })}>GIFs</a>
</div>

${this.state.activeTab === "stickers" && html`
<nav onWheel=${this.navScroll} ref=${elem => this.navRef = elem}>
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="recent" />
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
<${NavBarItem} pack=${{ id: "settings", title: "Settings" }} iconOverride="settings" />
</nav>

<${SearchBox} onKeyUp=${this.searchStickers} />
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${(elem) => (this.packListRef = elem)}>
${filterActive && packs.length === 0
? html`<div class="search-empty"><h1>No stickers match your search</h1></div>`
: null}
${packs.map((pack) => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)}
<${Settings} app=${this} />
</div>

`}
${this.state.activeTab === "gifs" && GIPHY_API_KEY !== "" && html`
<${GiphySearchTab} send=${this.sendGIF} />
`}
${this.state.activeTab === "gifs" && GIPHY_API_KEY === "" && html`
<h1><center>GIF Search is not enabled. Please enable it in the config.</center></h1>
`}
</main>`;
}
}

const Settings = ({ app }) => html`
Expand Down
4 changes: 4 additions & 0 deletions web/src/widget-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,7 @@ export function sendSticker(content) {
widgetData,
}, "*")
}

export function sendGIF(content) {
return sendSticker(content)
}
2 changes: 1 addition & 1 deletion web/style/index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions web/style/index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -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