Compare commits

...

10 commits

Author SHA1 Message Date
Alyx 4757273bff
Merge 9721925e28 into 90f06c511a 2024-05-09 14:24:56 +02:00
Lim Chee Aun 90f06c511a Test allow linking to post from generic accounts modal 2024-05-08 10:29:00 +08:00
Lim Chee Aun e7aad03279 Preliminary implementation of moderation_warning notifications 2024-05-08 10:28:34 +08:00
Lim Chee Aun 1c6b0aa0d7 Upgrade dependencies 2024-05-06 12:48:55 +08:00
Lim Chee Aun 3e1b9ff53d Apply filter context in compact status too 2024-05-02 23:29:01 +08:00
Alyx 9721925e28
Add basic instruction to run the container 2024-04-19 17:18:31 +02:00
Alyx e694de6255
Add Docker GHA
- As the build now happens inside the container, I thought it was a good idea to extract it from the image instead of rebuilding it on the host
- QEMU and Buildx are just stuff for multiplatform builds (even if it's just a bunch of static files, I don't think docker is able to detect that the base image (nginx) exists in multiplatform, I might be wrong
- Add actions to extract metadata in order to tag the image
- Add actions to logins to Docker Hub and GitHub Registry
- Build and push to both registry, with our previously created tags
- Build only, locally, the "artifacts" stage, where we can extract the zip files. As it's reusing the previous stages, it won't be build twice.
- Extract the artifacts so the previous release workflow can keep on going (as it should!)
2024-04-19 17:08:30 +02:00
Alyx 0f5f8dfd0f
Update Dockerfile:
- The build now happens in the container itself, uisng node:20-alpine as the build stage
- A special artifacts stage is created (but won't be used unless specifically called) to create the archive files, used as the releases
- Switched from httpd to nginx

The image is still very light (16.9MB)
2024-04-19 16:59:27 +02:00
Alyx e2228bfc8f
Add .dockerignore 2024-04-19 16:55:39 +02:00
Alyx fa196a2c94
Add Dockerfile 2024-03-18 09:52:29 +01:00
12 changed files with 1857 additions and 1479 deletions

38
.dockerignore Normal file
View file

@ -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

View file

@ -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 }}

36
Dockerfile Normal file
View file

@ -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

View file

@ -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

3011
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,7 @@
"@szhsin/react-menu": "~4.1.0",
"@uidotdev/usehooks": "~2.4.1",
"compare-versions": "~6.1.0",
"dayjs": "~1.11.10",
"dayjs": "~1.11.11",
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2",
"fast-equals": "~5.0.1",
@ -28,18 +28,18 @@
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.7.2",
"masto": "~6.7.7",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.20.2",
"preact": "~10.21.0",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-intersection-observer": "~9.8.2",
"react-intersection-observer": "~9.10.2",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
"string-length": "6.0.0",
"swiped-events": "~1.1.9",
"swiped-events": "~1.2.0",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~10.0.0",
@ -51,18 +51,18 @@
"@preact/preset-vite": "~2.8.2",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.38",
"postcss-dark-theme-class": "~1.2.3",
"postcss-preset-env": "~9.5.8",
"postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~9.5.11",
"twitter-text": "~3.1.0",
"vite": "~5.2.10",
"vite": "~5.2.11",
"vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.19.8",
"vite-plugin-pwa": "~0.20.0",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",
"workbox-routing": "~7.0.0",
"workbox-strategies": "~7.0.0"
"workbox-cacheable-response": "~7.1.0",
"workbox-expiration": "~7.1.0",
"workbox-routing": "~7.1.0",
"workbox-strategies": "~7.1.0"
},
"postcss": {
"plugins": {

View file

@ -17,6 +17,21 @@
);
filter: saturate(0.5);
}
&:is(a) {
pointer-events: auto;
display: block;
text-decoration: none;
color: inherit;
&:hover {
border-color: var(--outline-hover-color);
}
.status {
filter: none;
}
}
}
.accounts-list {

View file

@ -11,6 +11,7 @@ import useLocationChange from '../utils/useLocationChange';
import AccountBlock from './account-block';
import Icon from './icon';
import Link from './link';
import Loader from './loader';
import Status from './status';
@ -143,9 +144,12 @@ export default function GenericAccounts({
</header>
<main>
{post && (
<div class="post-preview">
<Link
to={`/${instance || currentInstance}/s/${post.id}`}
class="post-preview"
>
<Status status={post} size="s" readOnly />
</div>
</Link>
)}
{accounts.length > 0 ? (
<>

View file

@ -28,6 +28,7 @@ const NOTIFICATION_ICONS = {
'admin.signup': 'account-edit',
'admin.report': 'account-warning',
severed_relationships: 'heart-break',
moderation_warning: 'alert',
emoji_reaction: 'emoji2',
'pleroma:emoji_reaction': 'emoji2',
};
@ -45,6 +46,8 @@ poll = A poll you have voted in or created has ended
update = A status you interacted with has been edited
admin.sign_up = Someone signed up (optionally sent to admins)
admin.report = A new report has been filed
severed_relationships = Severed relationships
moderation_warning = Moderation warning
*/
function emojiText(emoji, emoji_url) {
@ -91,6 +94,7 @@ const contentText = {
Lost connections with <i>{name}</i>.
</>
),
moderation_warning: <b>Moderation warning</b>,
emoji_reaction: emojiText,
'pleroma:emoji_reaction': emojiText,
};
@ -117,6 +121,17 @@ const SEVERED_RELATIONSHIPS_TEXT = {
),
};
const MODERATION_WARNING_TEXT = {
none: 'Your account has received a moderation warning.',
disable: 'Your account has been disabled.',
mark_statuses_as_sensitive:
'Some of your posts have been marked as sensitive.',
delete_statuses: 'Some of your posts have been deleted.',
sensitive: 'Your posts will be marked as sensitive from now on.',
silence: 'Your account has been limited.',
suspend: 'Your account has been suspended.',
};
const AVATARS_LIMIT = 50;
function Notification({
@ -125,8 +140,16 @@ function Notification({
isStatic,
disableContextMenu,
}) {
const { id, status, account, report, event, _accounts, _statuses } =
notification;
const {
id,
status,
account,
report,
event,
moderation_warning,
_accounts,
_statuses,
} = notification;
let { type } = notification;
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
@ -314,6 +337,20 @@ function Notification({
.
</div>
)}
{type === 'moderation_warning' && !!moderation_warning && (
<div>
{MODERATION_WARNING_TEXT[moderation_warning.action]}
<br />
<a
href={`/disputes/strikes/${moderation_warning.id}`}
target="_blank"
rel="noopener noreferrer"
>
Learn more <Icon icon="external" size="s" />
</a>
.
</div>
)}
</>
)}
{_accounts?.length > 1 && (

View file

@ -569,8 +569,15 @@
font-weight: bold;
vertical-align: middle;
display: inline-block;
&.horizontal {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
}
}
.status-filtered-badge.badge-meta {
.status-filtered-badge:not(.horizontal).badge-meta {
display: inline-flex;
flex-direction: column;
position: relative;
@ -584,10 +591,10 @@
border-color: var(--text-color);
background: var(--bg-color);
}
.status-filtered-badge.badge-meta > span:first-child {
.status-filtered-badge:not(.horizontal).badge-meta > span:first-child {
white-space: nowrap;
}
.status-filtered-badge.badge-meta > span + span {
.status-filtered-badge:not(.horizontal).badge-meta > span + span {
display: block;
font-size: 9px;
font-weight: normal;
@ -601,6 +608,10 @@
left: 0;
text-align: center;
}
.status-filtered-badge.horizontal.badge-meta > span + span {
font-weight: normal;
text-transform: none;
}
.status.large > .container > .content-container {
margin-left: calc(-50px - 16px);

View file

@ -646,7 +646,11 @@ const TimelineItem = memo(
>
<Link class="status-link timeline-item" to={url}>
{showCompact ? (
<TimelineStatusCompact status={item} instance={instance} />
<TimelineStatusCompact
status={item}
instance={instance}
filterContext={filterContext}
/>
) : useItemID ? (
<Status
statusID={statusID}
@ -820,11 +824,12 @@ function StatusCarousel({ title, class: className, children }) {
);
}
function TimelineStatusCompact({ status, instance }) {
function TimelineStatusCompact({ status, instance, filterContext }) {
const snapStates = useSnapshot(states);
const { id, visibility, language } = status;
const statusPeekText = statusPeek(status);
const sKey = statusKey(id, instance);
const filterInfo = isFiltered(status.filtered, filterContext);
return (
<article
class={`status compact-thread ${
@ -850,13 +855,24 @@ function TimelineStatusCompact({ status, instance }) {
lang={language}
dir="auto"
>
{statusPeekText}
{status.sensitive && status.spoilerText && (
{!!filterInfo ? (
<b
class="status-filtered-badge badge-meta horizontal"
title={filterInfo?.titlesStr || ''}
>
<span>Filtered</span>: <span>{filterInfo?.titlesStr || ''}</span>
</b>
) : (
<>
{' '}
<span class="spoiler-badge">
<Icon icon="eye-close" size="s" />
</span>
{statusPeekText}
{status.sensitive && status.spoilerText && (
<>
{' '}
<span class="spoiler-badge">
<Icon icon="eye-close" size="s" />
</span>
</>
)}
</>
)}
</div>

View file

@ -102,6 +102,17 @@ function Notifications({ columnMode }) {
// },
// });
// TEST: Slot in a fake notification to test 'moderation_warning'
// notifications.unshift({
// id: '123123',
// type: 'moderation_warning',
// createdAt: new Date().toISOString(),
// moderation_warning: {
// id: '1231234',
// action: 'mark_statuses_as_sensitive',
// },
// });
// console.log({ notifications });
const groupedNotifications = groupNotifications(notifications);