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
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: production
|
||||
# - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
|
||||
# - 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
|
||||
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
|
||||
with:
|
||||
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">
|
||||
<img src="design/logo-4.svg" width="128" height="128" alt="">
|
||||
|
||||
Phanpy
|
||||
===
|
||||
# Phanpy
|
||||
|
||||
**Minimalistic opinionated Mastodon web client.**
|
||||
|
||||
</div>
|
||||
|
||||
![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]`.
|
||||
- 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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
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:
|
||||
|
||||
|
@ -222,7 +234,7 @@ Costs involved in running and developing this web app:
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ const HASHTAG_RE = new RegExp(
|
|||
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
|
||||
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
|
||||
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',
|
||||
);
|
||||
|
||||
|
@ -1219,22 +1219,30 @@ function Compose({
|
|||
/>
|
||||
<Icon icon="attachment" />
|
||||
</label>{' '}
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-button"
|
||||
disabled={
|
||||
uiState === 'loading' || !!poll || !!mediaAttachments.length
|
||||
}
|
||||
onClick={() => {
|
||||
setPoll({
|
||||
options: ['', ''],
|
||||
expiresIn: 24 * 60 * 60, // 1 day
|
||||
multiple: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="poll" alt="Add poll" />
|
||||
</button>{' '}
|
||||
{/* If maxOptions is not defined or defined and is greater than 1, show poll button */}
|
||||
{maxOptions == null ||
|
||||
(maxOptions > 1 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-button"
|
||||
disabled={
|
||||
uiState === 'loading' ||
|
||||
!!poll ||
|
||||
!!mediaAttachments.length
|
||||
}
|
||||
onClick={() => {
|
||||
setPoll({
|
||||
options: ['', ''],
|
||||
expiresIn: 24 * 60 * 60, // 1 day
|
||||
multiple: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="poll" alt="Add poll" />
|
||||
</button>{' '}
|
||||
</>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-button"
|
||||
|
|
|
@ -388,7 +388,7 @@ function Media({
|
|||
data-orientation="${orientation}"
|
||||
preload="auto"
|
||||
autoplay
|
||||
muted="${isGIF}"
|
||||
${isGIF ? 'muted' : ''}
|
||||
${isGIF ? '' : 'controls'}
|
||||
playsinline
|
||||
loop="${loopable}"
|
||||
|
|
|
@ -21,6 +21,7 @@ export default function RelativeTime({ datetime, format }) {
|
|||
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
||||
const date = useMemo(() => dayjs(datetime), [datetime]);
|
||||
const [dateStr, dt, title] = useMemo(() => {
|
||||
if (!date.isValid()) return ['' + datetime, '', ''];
|
||||
let str;
|
||||
if (format === 'micro') {
|
||||
// If date <= 1 day ago or day is within this year
|
||||
|
@ -37,6 +38,7 @@ export default function RelativeTime({ datetime, format }) {
|
|||
}, [date, format, renderCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!date.isValid()) return;
|
||||
let timeout;
|
||||
let raf;
|
||||
function rafRerender() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import Link from '../components/link';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import { fixNotifications } from '../utils/group-notifications';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
|
@ -30,6 +31,8 @@ function Mentions({ columnMode, ...props }) {
|
|||
const results = await mentionsIterator.current.next();
|
||||
let { value } = results;
|
||||
if (value?.length) {
|
||||
value = fixNotifications(value);
|
||||
|
||||
if (firstLoad) {
|
||||
latestItem.current = value[0].id;
|
||||
console.log('First load', latestItem.current);
|
||||
|
|
|
@ -9,7 +9,7 @@ const notificationTypeKeys = {
|
|||
poll: ['status'],
|
||||
update: ['status'],
|
||||
};
|
||||
function fixNotifications(notifications) {
|
||||
export function fixNotifications(notifications) {
|
||||
return notifications.filter((notification) => {
|
||||
const { type, id, createdAt } = notification;
|
||||
if (!type) {
|
||||
|
|
|
@ -107,10 +107,10 @@ export function getCurrentInstance() {
|
|||
return (currentInstance = instances[instance]);
|
||||
} catch (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
|
||||
store.local.del('instances');
|
||||
location.reload();
|
||||
// store.local.del('instances');
|
||||
// location.reload();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue