kopia lustrzana https://github.com/cheeaun/phanpy
Porównaj commity
12 Commity
ee6b482575
...
51d6e458d8
Autor | SHA1 | Data |
---|---|---|
Alyx | 51d6e458d8 | |
Alyx | 9721925e28 | |
Alyx | e694de6255 | |
Alyx | 0f5f8dfd0f | |
Alyx | e2228bfc8f | |
Lim Chee Aun | 7376cb1e99 | |
Lim Chee Aun | ffbae70178 | |
Lim Chee Aun | 9235d2c800 | |
Lim Chee Aun | 6ccefaebe1 | |
Lim Chee Aun | 5a448c8049 | |
Lim Chee Aun | 9bf77fa97a | |
Alyx | fa196a2c94 |
|
@ -0,0 +1,38 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
.env.dev
|
||||||
|
phanpy-dist.zip
|
||||||
|
phanpy-dist.tar.gz
|
||||||
|
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.github/
|
||||||
|
readme-assets/
|
||||||
|
README.md
|
||||||
|
.gitignore
|
||||||
|
.prettierrc
|
||||||
|
Dockerfile
|
|
@ -10,19 +10,84 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: production
|
ref: production
|
||||||
# - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
|
# - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
|
||||||
# - run: git push --tags
|
# - run: git push --tags
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- run: npm ci && npm run build
|
|
||||||
- run: cd dist && zip -r ../phanpy-dist.zip . && tar -czf ../phanpy-dist.tar.gz . && cd ..
|
|
||||||
- id: tag_name
|
- id: tag_name
|
||||||
run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD)
|
run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# @cheeaun: If you want to check out other ways to tag your Docker image:
|
||||||
|
# https://github.com/docker/metadata-action/blob/master/README.md
|
||||||
|
# I kept "tag_name" as the tag name for the Docker image for now
|
||||||
|
- name: Extract metadata for the Docker image
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.tag_name.outputs.tag_name }}
|
||||||
|
|
||||||
|
# @cheeaun: I think deploying to Docker Hub and GitHub is a good idea, to always have a fallback
|
||||||
|
# - name: Login to Docker Hub
|
||||||
|
# uses: docker/login-action@v3
|
||||||
|
# with:
|
||||||
|
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Source: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
# @cheeaun: I think this is a good idea to support multiple architectures
|
||||||
|
# Basically here: any Windows, Mac or Linux computers, and 32-bits Raspberry Pi
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Extract artifacts from the Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
# @cheeaun: And this is where we extract the artifacts from the Docker image
|
||||||
|
# The reason I'm extracting it this way, is that you don't depend on anything else than docker,
|
||||||
|
# and you don't always know if your CI runner will have the tools to zip or tar a directory.
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ github.repository }}:artifacts-latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
# Copy the artifacts files from the Docker container to the host
|
||||||
|
- run: |
|
||||||
|
docker create --name phanpy-artifacts ${{ github.repository }}:artifacts-latest
|
||||||
|
docker cp -q phanpy-artifacts:/root/phanpy/latest.zip ./dist/phanpy-dist.zip
|
||||||
|
docker cp -q phanpy-artifacts:/root/phanpy/latest.tar.gz ./dist/phanpy-dist.tar.gz
|
||||||
|
docker rm phanpy-artifacts
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@v1
|
- uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.tag_name.outputs.tag_name }}
|
tag_name: ${{ steps.tag_name.outputs.tag_name }}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
#############################################
|
||||||
|
# Install everything to build the application
|
||||||
|
#############################################
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /root/phanpy
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# Special stage to easily extract the app as a zip
|
||||||
|
##################################################
|
||||||
|
FROM alpine:3 AS artifacts
|
||||||
|
|
||||||
|
WORKDIR /root/phanpy
|
||||||
|
|
||||||
|
RUN apk add zip
|
||||||
|
COPY --from=build /root/phanpy/dist /root/phanpy/dist
|
||||||
|
|
||||||
|
# Outputs:
|
||||||
|
# - /root/phanpy/latest.zip
|
||||||
|
# - /root/phanpy/latest.tar.gz
|
||||||
|
RUN zip -r /root/phanpy/latest.zip dist && \
|
||||||
|
tar -czf /root/phanpy/latest.tar.gz dist
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# Copy the static files to a mininal web server image
|
||||||
|
#####################################################
|
||||||
|
FROM nginx:1-alpine-slim
|
||||||
|
|
||||||
|
ENV NGINX_ENTRYPOINT_QUIET_LOGS=1
|
||||||
|
COPY --chown=static:static --from=build /root/phanpy/dist /usr/share/nginx/html
|
26
README.md
26
README.md
|
@ -1,10 +1,10 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="design/logo-4.svg" width="128" height="128" alt="">
|
<img src="design/logo-4.svg" width="128" height="128" alt="">
|
||||||
|
|
||||||
Phanpy
|
# Phanpy
|
||||||
===
|
|
||||||
|
|
||||||
**Minimalistic opinionated Mastodon web client.**
|
**Minimalistic opinionated Mastodon web client.**
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
![Fancy screenshot](readme-assets/fancy-screenshot.jpg)
|
![Fancy screenshot](readme-assets/fancy-screenshot.jpg)
|
||||||
|
@ -55,7 +55,7 @@ Everything is designed and engineered following my taste and vision. This is a p
|
||||||
|
|
||||||
- On the timeline, the user name is displayed as `[NAME] @[username]`.
|
- On the timeline, the user name is displayed as `[NAME] @[username]`.
|
||||||
- For the `@[username]`, always exclude the instance domain name.
|
- For the `@[username]`, always exclude the instance domain name.
|
||||||
- If the `[NAME]` *looks the same* as the `@[username]`, then the `@[username]` is excluded as well.
|
- If the `[NAME]` _looks the same_ as the `@[username]`, then the `@[username]` is excluded as well.
|
||||||
|
|
||||||
### Boosts Carousel
|
### Boosts Carousel
|
||||||
|
|
||||||
|
@ -123,17 +123,29 @@ Some of these may change in the future. The front-end world is ever-changing.
|
||||||
|
|
||||||
This is a **pure static web app**. You can host it anywhere you want.
|
This is a **pure static web app**. You can host it anywhere you want.
|
||||||
|
|
||||||
Two ways (choose one):
|
Some examples:
|
||||||
|
|
||||||
### Easy way
|
### Using pre-built releases
|
||||||
|
|
||||||
Go to [Releases](https://github.com/cheeaun/phanpy/releases) and download the latest `phanpy-dist.zip` or `phanpy-dist.tar.gz`. It's pre-built so don't need to run any install/build commands. Extract it. Serve the folder of extracted files.
|
Go to [Releases](https://github.com/cheeaun/phanpy/releases) and download the latest `phanpy-dist.zip` or `phanpy-dist.tar.gz`. It's pre-built so don't need to run any install/build commands. Extract it. Serve the folder of extracted files.
|
||||||
|
|
||||||
|
### Using a Docker image
|
||||||
|
|
||||||
|
In your terminal, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker run -d -p 8080:80 cheeaun/phanpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to http://localhost:8080 and 🎉
|
||||||
|
|
||||||
|
Make sure to deploy the web app using a reverse proxy that make the connection secure (using HTTPS).
|
||||||
|
|
||||||
### Custom-build way
|
### Custom-build way
|
||||||
|
|
||||||
Requires [Node.js](https://nodejs.org/).
|
Requires [Node.js](https://nodejs.org/).
|
||||||
|
|
||||||
Download or `git clone` this repository. Use `production` branch for *stable* releases, `main` for *latest*. Build it by running `npm run build` (after `npm install`). Serve the `dist` folder.
|
Download or `git clone` this repository. Use `production` branch for _stable_ releases, `main` for _latest_. Build it by running `npm run build` (after `npm install`). Serve the `dist` folder.
|
||||||
|
|
||||||
Customization can be done by passing environment variables to the build command. Examples:
|
Customization can be done by passing environment variables to the build command. Examples:
|
||||||
|
|
||||||
|
@ -222,7 +234,7 @@ Costs involved in running and developing this web app:
|
||||||
|
|
||||||
## Mascot
|
## Mascot
|
||||||
|
|
||||||
[Phanpy](https://bulbapedia.bulbagarden.net/wiki/Phanpy_(Pok%C3%A9mon)) is a Ground-type Pokémon.
|
[Phanpy](<https://bulbapedia.bulbagarden.net/wiki/Phanpy_(Pok%C3%A9mon)>) is a Ground-type Pokémon.
|
||||||
|
|
||||||
## Maintainers + contributors
|
## Maintainers + contributors
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ const HASHTAG_RE = new RegExp(
|
||||||
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
|
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
|
||||||
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
|
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
|
||||||
const SCAN_RE = new RegExp(
|
const SCAN_RE = new RegExp(
|
||||||
`([^A-Za-z0-9_:\\n]|^)(:${SHORTCODE_RE_FRAGMENT}:)(?=[^A-Za-z0-9_:]|$)`,
|
`(^|[^=\\/\\w])(:${SHORTCODE_RE_FRAGMENT}:)(?=[^A-Za-z0-9_:]|$)`,
|
||||||
'g',
|
'g',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1219,22 +1219,30 @@ function Compose({
|
||||||
/>
|
/>
|
||||||
<Icon icon="attachment" />
|
<Icon icon="attachment" />
|
||||||
</label>{' '}
|
</label>{' '}
|
||||||
<button
|
{/* If maxOptions is not defined or defined and is greater than 1, show poll button */}
|
||||||
type="button"
|
{maxOptions == null ||
|
||||||
class="toolbar-button"
|
(maxOptions > 1 && (
|
||||||
disabled={
|
<>
|
||||||
uiState === 'loading' || !!poll || !!mediaAttachments.length
|
<button
|
||||||
}
|
type="button"
|
||||||
onClick={() => {
|
class="toolbar-button"
|
||||||
setPoll({
|
disabled={
|
||||||
options: ['', ''],
|
uiState === 'loading' ||
|
||||||
expiresIn: 24 * 60 * 60, // 1 day
|
!!poll ||
|
||||||
multiple: false,
|
!!mediaAttachments.length
|
||||||
});
|
}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
setPoll({
|
||||||
<Icon icon="poll" alt="Add poll" />
|
options: ['', ''],
|
||||||
</button>{' '}
|
expiresIn: 24 * 60 * 60, // 1 day
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="poll" alt="Add poll" />
|
||||||
|
</button>{' '}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="toolbar-button"
|
class="toolbar-button"
|
||||||
|
|
|
@ -388,7 +388,7 @@ function Media({
|
||||||
data-orientation="${orientation}"
|
data-orientation="${orientation}"
|
||||||
preload="auto"
|
preload="auto"
|
||||||
autoplay
|
autoplay
|
||||||
muted="${isGIF}"
|
${isGIF ? 'muted' : ''}
|
||||||
${isGIF ? '' : 'controls'}
|
${isGIF ? '' : 'controls'}
|
||||||
playsinline
|
playsinline
|
||||||
loop="${loopable}"
|
loop="${loopable}"
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default function RelativeTime({ datetime, format }) {
|
||||||
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
||||||
const date = useMemo(() => dayjs(datetime), [datetime]);
|
const date = useMemo(() => dayjs(datetime), [datetime]);
|
||||||
const [dateStr, dt, title] = useMemo(() => {
|
const [dateStr, dt, title] = useMemo(() => {
|
||||||
|
if (!date.isValid()) return ['' + datetime, '', ''];
|
||||||
let str;
|
let str;
|
||||||
if (format === 'micro') {
|
if (format === 'micro') {
|
||||||
// If date <= 1 day ago or day is within this year
|
// If date <= 1 day ago or day is within this year
|
||||||
|
@ -37,6 +38,7 @@ export default function RelativeTime({ datetime, format }) {
|
||||||
}, [date, format, renderCount]);
|
}, [date, format, renderCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!date.isValid()) return;
|
||||||
let timeout;
|
let timeout;
|
||||||
let raf;
|
let raf;
|
||||||
function rafRerender() {
|
function rafRerender() {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { fixNotifications } from '../utils/group-notifications';
|
||||||
import { saveStatus } from '../utils/states';
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -30,6 +31,8 @@ function Mentions({ columnMode, ...props }) {
|
||||||
const results = await mentionsIterator.current.next();
|
const results = await mentionsIterator.current.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
|
value = fixNotifications(value);
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
console.log('First load', latestItem.current);
|
console.log('First load', latestItem.current);
|
||||||
|
|
|
@ -9,7 +9,7 @@ const notificationTypeKeys = {
|
||||||
poll: ['status'],
|
poll: ['status'],
|
||||||
update: ['status'],
|
update: ['status'],
|
||||||
};
|
};
|
||||||
function fixNotifications(notifications) {
|
export function fixNotifications(notifications) {
|
||||||
return notifications.filter((notification) => {
|
return notifications.filter((notification) => {
|
||||||
const { type, id, createdAt } = notification;
|
const { type, id, createdAt } = notification;
|
||||||
if (!type) {
|
if (!type) {
|
||||||
|
|
|
@ -107,10 +107,10 @@ export function getCurrentInstance() {
|
||||||
return (currentInstance = instances[instance]);
|
return (currentInstance = instances[instance]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(`Failed to load instance configuration. Please try again.\n\n${e}`);
|
// alert(`Failed to load instance configuration. Please try again.\n\n${e}`);
|
||||||
// Temporary fix for corrupted data
|
// Temporary fix for corrupted data
|
||||||
store.local.del('instances');
|
// store.local.del('instances');
|
||||||
location.reload();
|
// location.reload();
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue