Merge remote-tracking branch 'origin/develop' into webv2
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
|
||||
# for details.
|
||||
|
||||
name: Build nightly docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
Docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: earthly/actions-setup@v1
|
||||
with:
|
||||
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
|
||||
|
||||
- name: Earthly version
|
||||
run: earthly --version
|
||||
|
||||
- name: Log into GitHub Container Registry
|
||||
env:
|
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
|
||||
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
if: env.GH_CR_PAT != null
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout and build
|
||||
if: env.GH_CR_PAT != null
|
||||
env:
|
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
|
||||
run: cd build/release && ./docker-nightly-earthly.sh
|
||||
@@ -1,24 +1,40 @@
|
||||
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
|
||||
# for details.
|
||||
|
||||
name: Build nightly docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 2 * * *"
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
Docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: earthly/actions-setup@v1
|
||||
with:
|
||||
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
|
||||
|
||||
- name: Earthly version
|
||||
run: earthly --version
|
||||
|
||||
- name: Log into GitHub Container Registry
|
||||
env:
|
||||
env:
|
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
|
||||
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
if: env.GH_CR_PAT != null
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup and run
|
||||
env:
|
||||
- name: Checkout and build
|
||||
if: env.GH_CR_PAT != null
|
||||
env:
|
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
|
||||
run: cd build/release && ./docker-nightly.sh
|
||||
if: env.GH_CR_PAT != null
|
||||
|
||||
@@ -178,11 +178,11 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error resolving publickey from iri")
|
||||
err = errors.Wrap(err, "error resolving publickey from iri, actor may not be valid: "+publicKeyIRI)
|
||||
}
|
||||
|
||||
if !resolved {
|
||||
err = errors.New("error resolving publickey from iri")
|
||||
err = errors.New("error resolving publickey from iri, actor may not be valid: " + publicKeyIRI)
|
||||
}
|
||||
|
||||
return pubkey, err
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Docker build
|
||||
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
|
||||
DOCKER_IMAGE="owncast-earthly"
|
||||
DATE=$(date +"%Y%m%d")
|
||||
VERSION="${DATE}-nightly"
|
||||
|
||||
echo "Building Docker image ${DOCKER_IMAGE}..."
|
||||
|
||||
# Change to the root directory of the repository
|
||||
cd $(git rev-parse --show-toplevel)
|
||||
|
||||
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"
|
||||
@@ -1,29 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Docker build
|
||||
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
|
||||
DOCKER_IMAGE="owncast"
|
||||
DATE=$(date +"%Y%m%d")
|
||||
VERSION="${DATE}-nightly"
|
||||
GIT_COMMIT=$(git rev-list -1 HEAD)
|
||||
|
||||
# Create production build of Tailwind CSS
|
||||
pushd ../../build/javascript >> /dev/null
|
||||
# Install the tailwind & postcss CLIs
|
||||
npm install --quiet --no-progress
|
||||
# Run the tailwind CLI and pipe it to postcss for minification.
|
||||
# Save it to a temp directory that we will reference below.
|
||||
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "../../webroot/js/web_modules/tailwindcss/dist/tailwind.min.css"
|
||||
popd
|
||||
|
||||
echo "Building Docker image ${DOCKER_IMAGE}..."
|
||||
|
||||
# Change to the root directory of the repository
|
||||
cd $(git rev-parse --show-toplevel)
|
||||
|
||||
# Docker build
|
||||
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t ghcr.io/owncast/${DOCKER_IMAGE}:nightly .
|
||||
|
||||
# Dockerhub
|
||||
# You must be authenticated via `docker login` with your Dockerhub credentials first.
|
||||
# docker push gabekangas/owncast:nightly
|
||||
|
||||
docker push ghcr.io/owncast/${DOCKER_IMAGE}:nightly
|
||||
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"
|
||||
|
||||
+3
-3
@@ -686,7 +686,7 @@ paths:
|
||||
type: string
|
||||
example: https://fediverse.biz/authorize_interaction?uri=https://my.owncast.server/federation/user/streamer
|
||||
|
||||
/api/chat/updatemessagevisibility:
|
||||
/api/chat/messagevisibility:
|
||||
post:
|
||||
summary: Update the visibility of chat messages.
|
||||
description: Pass an array of IDs you want to change the chat visibility of.
|
||||
@@ -951,7 +951,7 @@ paths:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
/api/admin/chat/updatemessagevisibility:
|
||||
/api/admin/chat/messagevisibility:
|
||||
post:
|
||||
summary: Update the visibility of chat messages.
|
||||
description: Pass an array of IDs you want to change the chat visibility of.
|
||||
@@ -1984,7 +1984,7 @@ paths:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
/api/integrations/chat/updatemessagevisibility:
|
||||
/api/integrations/chat/messagevisibility:
|
||||
post:
|
||||
summary: Update the visibility of chat messages.
|
||||
description: Pass an array of IDs you want to change the chat visibility of.
|
||||
|
||||
+2
-2
@@ -131,7 +131,7 @@ func Start() error {
|
||||
http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages))
|
||||
|
||||
// Update chat message visibility
|
||||
http.HandleFunc("/api/admin/chat/updatemessagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
|
||||
http.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
|
||||
|
||||
// Enable/disable a user
|
||||
http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled))
|
||||
@@ -318,7 +318,7 @@ func Start() error {
|
||||
// Inline chat moderation actions
|
||||
|
||||
// Update chat message visibility
|
||||
http.HandleFunc("/api/chat/updatemessagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
|
||||
http.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
|
||||
|
||||
// Enable/disable a user
|
||||
http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled))
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,112 +7,112 @@ const registerChat = require('./lib/chat').registerChat;
|
||||
const sendChatMessage = require('./lib/chat').sendChatMessage;
|
||||
|
||||
const testVisibilityMessage = {
|
||||
body: 'message ' + Math.floor(Math.random() * 100),
|
||||
type: 'CHAT',
|
||||
body: 'message ' + Math.floor(Math.random() * 100),
|
||||
type: 'CHAT',
|
||||
};
|
||||
|
||||
var messageId;
|
||||
|
||||
const establishedUserFailedChatMessage = {
|
||||
body: 'this message should fail to send ' + Math.floor(Math.random() * 100),
|
||||
type: 'CHAT',
|
||||
body: 'this message should fail to send ' + Math.floor(Math.random() * 100),
|
||||
type: 'CHAT',
|
||||
};
|
||||
|
||||
test('can send a chat message', async (done) => {
|
||||
const registration = await registerChat();
|
||||
const accessToken = registration.accessToken;
|
||||
const registration = await registerChat();
|
||||
const accessToken = registration.accessToken;
|
||||
|
||||
sendChatMessage(testVisibilityMessage, accessToken, done);
|
||||
sendChatMessage(testVisibilityMessage, accessToken, done);
|
||||
});
|
||||
|
||||
test('verify we can make API call to mark message as hidden', async (done) => {
|
||||
const registration = await registerChat();
|
||||
const accessToken = registration.accessToken;
|
||||
const ws = new WebSocket(
|
||||
`ws://localhost:8080/ws?accessToken=${accessToken}`,
|
||||
{
|
||||
origin: 'http://localhost:8080',
|
||||
}
|
||||
);
|
||||
const registration = await registerChat();
|
||||
const accessToken = registration.accessToken;
|
||||
const ws = new WebSocket(
|
||||
`ws://localhost:8080/ws?accessToken=${accessToken}`,
|
||||
{
|
||||
origin: 'http://localhost:8080',
|
||||
}
|
||||
);
|
||||
|
||||
// Verify the visibility change comes through the websocket
|
||||
ws.on('message', async function incoming(message) {
|
||||
const messages = message.split('\n');
|
||||
messages.forEach(async function (message) {
|
||||
const event = JSON.parse(message);
|
||||
// Verify the visibility change comes through the websocket
|
||||
ws.on('message', async function incoming(message) {
|
||||
const messages = message.split('\n');
|
||||
messages.forEach(async function (message) {
|
||||
const event = JSON.parse(message);
|
||||
|
||||
if (event.type === 'VISIBILITY-UPDATE') {
|
||||
ws.close();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
if (event.type === 'VISIBILITY-UPDATE') {
|
||||
ws.close();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const res = await request
|
||||
.get('/api/admin/chat/messages')
|
||||
.auth('admin', 'abc123')
|
||||
.expect(200);
|
||||
const res = await request
|
||||
.get('/api/admin/chat/messages')
|
||||
.auth('admin', 'abc123')
|
||||
.expect(200);
|
||||
|
||||
const message = res.body[0];
|
||||
messageId = message.id;
|
||||
await request
|
||||
.post('/api/admin/chat/updatemessagevisibility')
|
||||
.auth('admin', 'abc123')
|
||||
.send({ idArray: [messageId], visible: false })
|
||||
.expect(200);
|
||||
const message = res.body[0];
|
||||
messageId = message.id;
|
||||
await request
|
||||
.post('/api/admin/chat/messagevisibility')
|
||||
.auth('admin', 'abc123')
|
||||
.send({ idArray: [messageId], visible: false })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
test('verify message has become hidden', async (done) => {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const res = await request
|
||||
.get('/api/admin/chat/messages')
|
||||
.expect(200)
|
||||
.auth('admin', 'abc123');
|
||||
const res = await request
|
||||
.get('/api/admin/chat/messages')
|
||||
.expect(200)
|
||||
.auth('admin', 'abc123');
|
||||
|
||||
const message = res.body.filter((obj) => {
|
||||
return obj.id === messageId;
|
||||
});
|
||||
expect(message.length).toBe(1);
|
||||
// expect(message[0].hiddenAt).toBeTruthy();
|
||||
done();
|
||||
const message = res.body.filter((obj) => {
|
||||
return obj.id === messageId;
|
||||
});
|
||||
expect(message.length).toBe(1);
|
||||
// expect(message[0].hiddenAt).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
test('can enable established chat user mode', async (done) => {
|
||||
await request
|
||||
.post('/api/admin/config/chat/establishedusermode')
|
||||
.auth('admin', 'abc123')
|
||||
.send({ value: true })
|
||||
.expect(200);
|
||||
done();
|
||||
await request
|
||||
.post('/api/admin/config/chat/establishedusermode')
|
||||
.auth('admin', 'abc123')
|
||||
.send({ value: true })
|
||||
.expect(200);
|
||||
done();
|
||||
});
|
||||
|
||||
test('can send a message after established user mode is enabled', async (done) => {
|
||||
const registration = await registerChat();
|
||||
const accessToken = registration.accessToken;
|
||||
const registration = await registerChat();
|
||||
const accessToken = registration.accessToken;
|
||||
|
||||
sendChatMessage(establishedUserFailedChatMessage, accessToken, done);
|
||||
sendChatMessage(establishedUserFailedChatMessage, accessToken, done);
|
||||
});
|
||||
|
||||
test('verify rejected message is not in the chat feed', async (done) => {
|
||||
const res = await request
|
||||
.get('/api/admin/chat/messages')
|
||||
.expect(200)
|
||||
.auth('admin', 'abc123');
|
||||
const res = await request
|
||||
.get('/api/admin/chat/messages')
|
||||
.expect(200)
|
||||
.auth('admin', 'abc123');
|
||||
|
||||
const message = res.body.filter((obj) => {
|
||||
return obj.body === establishedUserFailedChatMessage.body;
|
||||
});
|
||||
const message = res.body.filter((obj) => {
|
||||
return obj.body === establishedUserFailedChatMessage.body;
|
||||
});
|
||||
|
||||
expect(message.length).toBe(0);
|
||||
done();
|
||||
expect(message.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
test('can disable established chat user mode', async (done) => {
|
||||
await request
|
||||
.post('/api/admin/config/chat/establishedusermode')
|
||||
.auth('admin', 'abc123')
|
||||
.send({ value: false })
|
||||
.expect(200);
|
||||
done();
|
||||
await request
|
||||
.post('/api/admin/config/chat/establishedusermode')
|
||||
.auth('admin', 'abc123')
|
||||
.send({ value: false })
|
||||
.expect(200);
|
||||
done();
|
||||
});
|
||||
|
||||
Generated
+216
-872
File diff suppressed because it is too large
Load Diff
Generated
+254
-897
File diff suppressed because it is too large
Load Diff
Generated
+3
-3
@@ -1188,9 +1188,9 @@
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
|
||||
@@ -7,7 +7,7 @@ export const URL_CONFIG = `/api/config`;
|
||||
export const URL_VIEWER_PING = `/api/ping`;
|
||||
|
||||
// inline moderation actions
|
||||
export const URL_HIDE_MESSAGE = `/api/chat/updatemessagevisibility`;
|
||||
export const URL_HIDE_MESSAGE = `/api/chat/messagevisibility`;
|
||||
export const URL_BAN_USER = `/api/chat/users/setenabled`;
|
||||
|
||||
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
||||
|
||||
@@ -3,132 +3,133 @@ import htm from '/js/web_modules/htm.js';
|
||||
import { URL_FOLLOWERS } from '/js/utils/constants.js';
|
||||
const html = htm.bind(h);
|
||||
export default class FollowerList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
followers: [],
|
||||
followersPage: 0,
|
||||
currentPage: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
this.state = {
|
||||
followers: [],
|
||||
followersPage: 0,
|
||||
currentPage: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
try {
|
||||
this.getFollowers();
|
||||
} catch (e) {
|
||||
console.error('followers error: ', e);
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
try {
|
||||
this.getFollowers();
|
||||
} catch (e) {
|
||||
console.error('followers error: ', e);
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowers() {
|
||||
const { currentPage } = this.state;
|
||||
const limit = 24;
|
||||
const offset = currentPage * limit;
|
||||
const u = `${URL_FOLLOWERS}?offset=${offset}&limit=${limit}`;
|
||||
const response = await fetch(u);
|
||||
const followers = await response.json();
|
||||
async getFollowers(requestedPage) {
|
||||
const limit = 24;
|
||||
const offset = requestedPage * limit;
|
||||
const u = `${URL_FOLLOWERS}?offset=${offset || 0}&limit=${limit}`;
|
||||
const response = await fetch(u);
|
||||
const followers = await response.json();
|
||||
const pages = Math.ceil(followers.total / limit);
|
||||
|
||||
this.setState({
|
||||
followers: followers.results,
|
||||
total: response.total,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
followers: followers.results,
|
||||
total: followers.total,
|
||||
pages: pages,
|
||||
});
|
||||
}
|
||||
|
||||
changeFollowersPage(page) {
|
||||
this.setState({ currentPage: page });
|
||||
this.getFollowers();
|
||||
}
|
||||
changeFollowersPage(requestedPage) {
|
||||
this.setState({ currentPage: requestedPage });
|
||||
this.getFollowers(requestedPage);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { followers, total, currentPage } = this.state;
|
||||
if (!followers) {
|
||||
return null;
|
||||
}
|
||||
render() {
|
||||
const { followers, total, pages, currentPage } = this.state;
|
||||
if (!followers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const noFollowersInfo = html`<div class="col-span-4">
|
||||
<p class="mb-5 text-2xl">Be the first to follow this live stream.</p>
|
||||
<p class="text-md">
|
||||
By following this stream you'll get updates when it goes live, receive
|
||||
posts from the streamer, and be featured here as a follower.
|
||||
</p>
|
||||
<p class="text-md mt-5">
|
||||
Learn more about ${' '}
|
||||
<a class="underline" href="https://en.wikipedia.org/wiki/Fediverse"
|
||||
>The Fediverse</a
|
||||
>, where you can follow this server as well as so much more.
|
||||
</p>
|
||||
</div>`;
|
||||
const noFollowersInfo = html`<div class="col-span-4">
|
||||
<p class="mb-5 text-2xl">Be the first to follow this live stream.</p>
|
||||
<p class="text-md">
|
||||
By following this stream you'll get updates when it goes live, receive
|
||||
posts from the streamer, and be featured here as a follower.
|
||||
</p>
|
||||
<p class="text-md mt-5">
|
||||
Learn more about ${' '}
|
||||
<a class="underline" href="https://en.wikipedia.org/wiki/Fediverse"
|
||||
>The Fediverse</a
|
||||
>, where you can follow this server as well as so much more.
|
||||
</p>
|
||||
</div>`;
|
||||
|
||||
const paginationControls =
|
||||
total > 1 &&
|
||||
Array(total)
|
||||
.fill()
|
||||
.map((x, n) => {
|
||||
const activePageClass =
|
||||
n === currentPage &&
|
||||
'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white';
|
||||
return html` <li class="page-item active w-10">
|
||||
<a
|
||||
class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}"
|
||||
onClick=${() => this.changeFollowersPage(n)}
|
||||
>
|
||||
${n + 1}
|
||||
</a>
|
||||
</li>`;
|
||||
});
|
||||
const paginationControls =
|
||||
pages > 1 &&
|
||||
Array(pages)
|
||||
.fill()
|
||||
.map((x, n) => {
|
||||
const activePageClass =
|
||||
n === currentPage &&
|
||||
'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white';
|
||||
return html` <li class="page-item active w-10">
|
||||
<a
|
||||
class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}"
|
||||
onClick=${() => this.changeFollowersPage(n)}
|
||||
>
|
||||
${n + 1}
|
||||
</a>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
>
|
||||
${followers.length === 0 && noFollowersInfo}
|
||||
${followers.map((follower) => {
|
||||
return html` <${SingleFollower} user=${follower} /> `;
|
||||
})}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<nav aria-label="Tab pages">
|
||||
<ul class="flex list-style-none flex-wrap">
|
||||
${paginationControls}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
>
|
||||
${followers.length === 0 && noFollowersInfo}
|
||||
${followers.map((follower) => {
|
||||
return html` <${SingleFollower} user=${follower} /> `;
|
||||
})}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<nav aria-label="Tab pages">
|
||||
<ul class="flex list-style-none flex-wrap">
|
||||
${paginationControls}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function SingleFollower(props) {
|
||||
const { user } = props;
|
||||
const { name, username, link, image } = user;
|
||||
const { user } = props;
|
||||
const { name, username, link, image } = user;
|
||||
|
||||
var displayName = name;
|
||||
var displayUsername = username;
|
||||
var displayName = name;
|
||||
var displayUsername = username;
|
||||
|
||||
if (!displayName) {
|
||||
displayName = displayUsername.split('@', 1)[0];
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
href=${link}
|
||||
class="following-list-follower block bg-white flex p-2 rounded-xl shadow border hover:no-underline mb-3 mr-3"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="${image || '/img/logo.svg'}"
|
||||
class="w-16 h-16 rounded-full"
|
||||
onError=${({ currentTarget }) => {
|
||||
currentTarget.onerror = null;
|
||||
currentTarget.src = '/img/logo.svg';
|
||||
}}
|
||||
/>
|
||||
<div class="p-3 truncate flex-grow">
|
||||
<p class="font-semibold text-gray-700 truncate">${displayName}</p>
|
||||
<p class="text-sm text-gray-500 truncate">${displayUsername}</p>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
if (!displayName) {
|
||||
displayName = displayUsername.split('@', 1)[0];
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
href=${link}
|
||||
class="following-list-follower block bg-white flex p-2 rounded-xl shadow border hover:no-underline mb-3 mr-3"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="${image || '/img/logo.svg'}"
|
||||
class="w-16 h-16 rounded-full"
|
||||
onError=${({ currentTarget }) => {
|
||||
currentTarget.onerror = null;
|
||||
currentTarget.src = '/img/logo.svg';
|
||||
}}
|
||||
/>
|
||||
<div class="p-3 truncate flex-grow">
|
||||
<p class="font-semibold text-gray-700 truncate">${displayName}</p>
|
||||
<p class="text-sm text-gray-500 truncate">${displayUsername}</p>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user