Browse Source

migrating from Shing to OurVoice (wip)

develop
Rob Colbert 3 months ago
parent
commit
dbe951f415
  1. 67
      app/services/gab-tv.js
  2. 20
      app/services/venue.js
  3. 7
      app/views/admin/settings/editor.pug
  4. 10
      app/views/admin/venue/channel/editor.pug
  5. 2
      app/views/components/off-canvas.pug
  6. 14
      app/views/components/page-footer.pug
  7. 49
      app/views/components/page-sidebar.pug
  8. 2
      app/views/newsroom/search.pug
  9. 2
      app/views/newsroom/unified-feed.pug
  10. 4
      app/views/venue/embed.pug
  11. 63
      client/img/social-icons/ourvoice.svg
  12. 2
      docs/docker.md
  13. 2
      dtp-sites.js
  14. 2
      static/502.html
  15. 29
      static/offline.html

67
app/services/gab-tv.js

@ -1,67 +0,0 @@
// gab-tv.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const fetch = require('node-fetch'); // jshint ignore:line
const CACHE_DURATION = 60 * 5;
const { SiteService } = require('../../lib/site-lib');
class GabTVService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
channelMiddleware ( ) {
return async (req, res, next) => {
try {
if (!res.locals.site || !res.locals.site.gabtvUrl) {
return next();
}
this.log.debug('GabTV URL', { url: res.locals.site.gabtvUrl });
const urlParts = res.locals.site.gabtvUrl.split('/');
const channelSlug = urlParts[urlParts.length - 1];
res.locals.gabTvChannel = await this.getChannelEpisodes(channelSlug, { allowCache: true });
return next();
} catch (error) {
this.log.error('failed to populdate Gab TV channel', { error });
return next();
}
};
}
async getChannelEpisodes (channelSlug, options) {
const { cache: cacheService } = this.dtp.services;
const cacheKey = `gabtv:ch:${channelSlug}`;
options = Object.assign({
allowCache: true,
}, options);
let json;
if (options.allowCache) {
json = await cacheService.getObject(cacheKey);
if (json) {
return json;
}
}
const response = await fetch(`https://tv.gab.com/channel/${channelSlug}/feed/json`);
json = await response.json();
await cacheService.setObjectEx(cacheKey, CACHE_DURATION, json);
return json;
}
}
module.exports = {
logId: 'svc:gab-tv',
index: 'gabTV',
className: 'GabTVService',
create: (dtp) => { return new GabTVService(dtp); },
};

20
app/services/venue.js

@ -24,7 +24,7 @@ class VenueService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
this.soapboxDomain = process.env.DTP_SOAPBOX_HOST || 'shing.tv';
this.streamrayDomain = process.env.DTP_STREAMRAY_HOST || 'ourvoice.stream';
}
async start ( ) {
@ -70,7 +70,7 @@ class VenueService extends SiteService {
});
return next();
} catch (error) {
this.log.error('failed to populate Soapbox channel data for route', { error });
this.log.error('failed to populate StreamRay channel data for route', { error });
return next();
}
};
@ -118,8 +118,8 @@ class VenueService extends SiteService {
updateOp.$set.slug = this.getChannelSlug(channelDefinition.url);
updateOp.$set.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10);
updateOp.$set.name = status.name;
updateOp.$set.description = status.description;
updateOp.$set.name = channelDefinition.name;
updateOp.$set.description = channelDefinition.description;
if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
throw new SiteError(400, 'Must provide a stream key');
@ -194,8 +194,8 @@ class VenueService extends SiteService {
if (json) { return json; }
}
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/feed/json`;
this.log.info('fetching Shing channel feed', { slug: channel.slug, requestUrl });
const requestUrl = `https://${this.streamrayDomain}/channel/${channel.slug}/feed/json`;
this.log.info('fetching StreamRay channel feed', { slug: channel.slug, requestUrl });
const response = await fetch(requestUrl, {
agent: this.httpsAgent,
headers: {
@ -204,7 +204,7 @@ class VenueService extends SiteService {
},
});
if (!response.ok) {
throw new SiteError(500, `Failed to fetch Shing channel feed: ${response.statusText}`);
throw new SiteError(500, `Failed to fetch StreamRay channel feed: ${response.statusText}`);
}
json = await response.json();
@ -216,8 +216,8 @@ class VenueService extends SiteService {
async updateChannelStatus (channel) {
const { logan: loganService } = this.dtp.services;
try {
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`;
this.log.debug('fetching Shing channel status', { slug: channel.slug, requestUrl });
const requestUrl = `https://${this.streamrayDomain}/channel/${channel.slug}/status`;
this.log.debug('fetching StreamRay channel status', { slug: channel.slug, requestUrl });
const response = await fetch(requestUrl, {
agent: this.httpsAgent,
@ -250,7 +250,7 @@ class VenueService extends SiteService {
getChannelSlug (channelUrl) {
const { URL } = require('url');
const url = new URL(channelUrl);
if (url.host !== this.soapboxDomain) {
if (url.host !== this.streamrayDomain) {
throw new SiteError(400, 'This is not a valid DTP stream channel URL: Domain mismatch.');
}

7
app/views/admin/settings/editor.pug

@ -35,11 +35,8 @@ block content
label(for="gab-url").uk-form-label Gab Social Profile
input(id="gab-url", name="gabUrl", type="url", placeholder="Enter Gab profile URL", value= site.gabUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="gabtv-url").uk-form-label Gab TV Channel
input(id="gabtv-url", name="gabtvUrl", type="url", placeholder="Enter Gab TV URL", value= site.gabtvUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="shing-url").uk-form-label Shing URL
input(id="shing-url", name="shingUrl", type="url", placeholder="Enter Shing URL", value= site.shingUrl).uk-input
label(for="streamray-url").uk-form-label StreamRay URL
input(id="streamray-url", name="streamrayUrl", type="url", placeholder="Enter StreamRay URL", value= site.streamrayUrl).uk-input
fieldset
legend Social links

10
app/views/admin/venue/channel/editor.pug

@ -8,13 +8,13 @@ block content
.uk-card-header
h1.uk-card-title= channel ? 'Update Channel' : 'Create Channel'
p You are linking a Shing.tv stream channel to your Venue. Venue wants the Shing channel information, but wants the local user information for the owner of the channel. You are tying a remote Shing channel to a local user of #{site.name}.
p You are linking a #{dtp.services.venue.streamrayDomain} stream channel to your Venue. Venue wants the StreamRay channel information, but wants the local user information for the owner of the channel. You are tying a remote StreamRay channel to a local user of #{site.name}.
.uk-card-body
.uk-margin
label(for="slug").uk-form-label Channel URL
input(type="url", name="url", placeholder="Paste Shing.tv channel URL", value= channel ? `https://${dtp.services.venue.soapboxDomain}/channel/${channel.slug}` : undefined).uk-input
.uk-text-small.uk-text-muted #{site.name} integrates #{dtp.services.venue.soapboxDomain} and wants the channel URL from there.
input(type="url", name="url", placeholder= `Paste ${dtp.services.venue.streamrayDomain} channel URL`, value= channel ? `https://${dtp.services.venue.streamrayDomain}/channel/${channel.slug}` : undefined).uk-input
.uk-text-small.uk-text-muted #{site.name} integrates #{dtp.services.venue.streamrayDomain} and wants the channel URL from there.
div(uk-grid)
div(class="uk-width-1-1 uk-width-2-3@m")
@ -25,7 +25,7 @@ block content
id="stream-key",
name="credentials.streamKey",
type="text",
placeholder="Paste Shing.tv stream key",
placeholder= `Paste ${dtp.services.venue.streamrayDomain} stream key`,
data-key-value= channel ? channel.credentials.streamKey : undefined,
).uk-input
.uk-width-auto
@ -45,7 +45,7 @@ block content
id="widget-key",
name="credentials.widgetKey",
type="text",
placeholder="Paste Shing.tv widget key",
placeholder= `Paste ${dtp.services.venue.streamrayDomain} widget key`,
data-key-value= channel ? channel.credentials.widgetKey : undefined,
).uk-input
.uk-width-auto

2
app/views/components/off-canvas.pug

@ -23,7 +23,7 @@ mixin renderMenuItem (iconClass, label)
a(href='/').uk-display-block
+renderMenuItem('fa-home', 'Home')
if site.shingWidgetKey
if site.streamrayWidgetKey
li(class={ "uk-active": (currentView === 'venue') })
a(href='/venue').uk-display-block
+renderMenuItem('fa-tv', 'Watch Live')

14
app/views/components/page-footer.pug

@ -8,12 +8,12 @@ section.uk-section.uk-section-muted.uk-section-xsmall.dtp-site-footer
.uk-container.uk-text-small.uk-text-center
.uk-margin
ul.uk-subnav.uk-flex-center
if site.shingUrl
if site.streamrayUrl
li
a(href= site.shingUrl, target="_blank").dtp-social-link
a(href= site.streamrayUrl, target="_blank").dtp-social-link
span
img(src="/img/social-icons/shing.png", style="width: auto; height: 1em;")
span.uk-margin-small-left Shing
img(src="/img/social-icons/ourvoice.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left OurVoice
if site.gabUrl
li
@ -21,12 +21,6 @@ section.uk-section.uk-section-muted.uk-section-xsmall.dtp-site-footer
span
img(src="/img/icon/gab-g.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left Gab Social
if site.gabtvUrl
li
a(href= site.gabtvUrl, target="_blank").dtp-social-link
span
img(src="/img/icon/gab-g.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left Gab TV
if site.telegramUrl
li

49
app/views/components/page-sidebar.pug

@ -5,23 +5,7 @@ include ../venue/components/channel-card
include ../venue/components/channel-list-item
include ../post/components/author-credit
- var isLive = !!shingChannelStatus && shingChannelStatus.isLive && !!shingChannelStatus.liveEpisode;
mixin renderSidebarEpisode(episode)
.uk-card.uk-card-default.uk-card-small.uk-card-hover
.uk-card-media-top
a(href= episode.url, target="_blank", title="Watch on Gab TV")
img(src=episode.image).responsive
a(
href= episode.url,
uk-tooltip=`Watch ${episode.title} on Gab TV`,
target="_blank",
).uk-link-reset.uk-display-block
.uk-card-body
.uk-text-bold.uk-text-truncate= episode.title
.uk-text-small= moment(episode.date_modified).format("MMM DD YYYY HH:MM a")
- var isLive = !!streamrayChannelStatus && streamrayChannelStatus.isLive && !!streamrayChannelStatus.liveEpisode;
mixin renderPageSidebar ( )
//-
@ -34,7 +18,7 @@ mixin renderPageSidebar ( )
+renderAnnouncement(announcement)
//-
//- Shing.tv Channel Integration
//- StreamRay Channel Integration
//-
-
@ -60,45 +44,30 @@ mixin renderPageSidebar ( )
+renderVenueChannelListItem(venueChannel)
//-
//- Shing.tv Channel Feed
//- StreamRay Channel Feed
//-
if shingChannelFeed && Array.isArray(shingChannelFeed.items) && (shingChannelFeed.items.length > 0)
if streamrayChannelFeed && Array.isArray(streamrayChannelFeed.items) && (streamrayChannelFeed.items.length > 0)
.uk-margin-medium
+renderSectionTitle(shingChannelFeed.title, {
+renderSectionTitle(streamrayChannelFeed.title, {
label: 'Tune In',
title: shingChannelFeed.title,
title: streamrayChannelFeed.title,
url: '/venue',
})
ul.uk-list
each item in shingChannelFeed.items.slice(0, 3)
each item in streamrayChannelFeed.items.slice(0, 3)
li
a(href= item.url, uk-tooltip=`Watch ${item.title} on Shing.tv`).uk-link-reset.uk-display-block
a(href= item.url, uk-tooltip= `Watch ${item.title} on ${dtp.services.venue.streamrayDomain}`).uk-link-reset.uk-display-block
.uk-card.uk-card-default.uk-card-small
img(src= item.image.url, width="640", height="360", alt=`Thumbnail image for ${item.title}`).responsive
.uk-card-body
div(uk-grid).uk-grid-small
.uk-width-auto
img(src=`https://${dtp.services.venue.soapboxDomain}/channel/${site.shingChannelSlug}/app-icon?s=48`)
img(src=`https://${dtp.services.venue.streamrayDomain}/channel/${site.streamrayChannelSlug}/app-icon?s=48`)
.uk-width-expand
.uk-text-bold.uk-text-truncate= item.title
.uk-text-small= moment(item.date_modified).format("MMM DD YYYY hh:mm a")
//- .uk-text-small!= item.summary
//-
//- Gab TV channel integration
//-
if gabTvChannel
.uk-margin-medium
+renderSectionTitle('Gab TV', {
label: 'Visit Channel',
title: gabTvChannel.title,
url: gabTvChannel.home_page_url,
})
ul.uk-list
each episode in gabTvChannel.items.slice(0, 3)
li
+renderSidebarEpisode(episode)
//-
//- Newsroom Integration
//-

2
app/views/newsroom/search.pug

@ -26,7 +26,7 @@ block content
each entry in newsroom.entries
li(data-entry-id= entry._id)
.uk-text-large.uk-text-bold.uk-margin-small
a(href= entry.link, target="shing_reader")= entry.title
a(href= entry.link, target="streamray_reader")= entry.title
.uk-margin-small= entry.description
div(uk-grid).uk-text-small
.uk-width-auto

2
app/views/newsroom/unified-feed.pug

@ -18,7 +18,7 @@ block content
each entry in newsroom.entries
li
.uk-text-large.uk-text-bold.uk-margin-small
a(href= entry.link, target="shing_reader")= entry.title
a(href= entry.link, target="streamray_reader")= entry.title
.uk-margin-small= entry.description
.uk-text-small source: #[a(href= entry.feed.link, target="_blank")= entry.feed.title]
else

4
app/views/venue/embed.pug

@ -6,10 +6,10 @@ block content
flex-grow: 1;
}
- var shingBaseUrl = `https://${dtp.services.venue.soapboxDomain}`;
- var streamrayBaseUrl = `https://${dtp.services.venue.streamrayDomain}`;
div(style="position: fixed; top: 64px; right: 0; bottom: 0; left: 0; width: 100%;").uk-flex.uk-flex-column
iframe(
src= `${shingBaseUrl}/channel/${channel.slug}/embed/venue?k=${channelCredentials.widgetKey}`,
src= `${streamrayBaseUrl}/channel/${channel.slug}/embed/venue?k=${channelCredentials.widgetKey}`,
allowfullscreen,
).embedded-frame

63
client/img/social-icons/ourvoice.svg

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="12in"
height="12in"
viewBox="0 0 304.8 304.80001"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="ourvoice.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="455.49629"
inkscape:cy="591.9799"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="in"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,7.8000118)">
<path
style="fill:#ffffff;stroke:#000000;stroke-width:0.92411059;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 91.380581,242.79719 c -6.805608,-2.28773 -11.971136,-6.26464 -16.243401,-12.50575 -3.626747,-5.29806 -5.277432,-10.66218 -5.610494,-18.23191 -0.103192,-2.34507 -1.303612,-2.75193 -6.115521,-2.07275 -2.358958,0.33296 -6.397067,1.17395 -8.973579,1.86889 -4.287761,1.15648 -5.112702,1.21554 -9.738961,0.69724 -5.879,-0.65871 -9.250954,-2.12097 -13.167407,-5.71016 -2.902341,-2.65982 -4.325872,-5.06408 -5.839781,-9.863 -1.185954,-3.75934 -1.192973,-3.95102 -0.34303,-9.36093 0.843249,-5.36729 0.932957,-5.61811 3.182292,-8.89633 3.15213,-4.59401 5.420961,-6.28347 13.565849,-10.10173 6.997353,-3.28031 9.024849,-4.57061 29.881212,-19.0159 5.743893,-3.97828 19.099811,-13.22496 29.67985,-20.54818 10.58003,-7.32326 21.465,-14.87244 24.18879,-16.77601 2.72378,-1.90356 13.3594,-9.26876 23.63469,-16.367117 10.27529,-7.098344 21.15993,-14.645404 24.18804,-16.771254 16.41453,-11.523523 30.62576,-21.22347 30.7813,-21.009946 0.0974,0.133538 -0.43519,1.666936 -1.18312,3.407572 -1.11486,2.594275 -1.68368,5.233143 -3.15556,14.638913 -1.55351,9.92355 -1.77018,12.272472 -1.60786,17.381765 1.10456,34.773007 13.0882,70.548097 31.95512,95.396577 5.88977,7.75709 10.94476,12.8111 16.99929,16.99609 2.50573,1.73196 4.42263,3.24606 4.25983,3.36467 -0.16286,0.11861 -4.98444,0.20733 -10.71476,0.19717 -5.73029,-0.01 -21.10631,-0.0393 -34.16891,-0.0647 -44.41003,-0.0864 -60.97576,-0.0531 -61.79587,0.12398 -1.40427,0.30329 -2.0645,1.97531 -2.55225,6.46344 -1.38735,12.76587 -10.49547,23.73182 -22.51252,27.10438 -4.51974,1.26846 -7.76279,1.57643 -15.40661,1.46308 -7.785539,-0.11543 -8.402899,-0.20009 -13.186629,-1.80814 z m 17.839929,-5.98513 c 13.28542,-0.26243 23.64184,-8.61807 25.52307,-20.59213 0.99348,-6.32354 0.28091,-6.90965 -8.37369,-6.88779 -12.54755,0.0317 -34.948179,-0.002 -40.92495,-0.0617 -7.665443,-0.0765 -8.298764,0.2609 -8.100536,4.31543 0.225981,4.62147 2.06959,9.47143 5.052769,13.29227 2.835608,3.63184 4.874395,5.33746 8.851518,7.405 2.9991,1.55911 10.886059,2.66896 17.971819,2.52899 z M 31.293846,215.7031 c -6.642376,-2.83635 -11.527577,-8.1625 -13.560167,-14.78425 -3.101656,-10.10452 -1.381837,-19.25021 4.951818,-26.33288 2.078427,-2.32424 6.40532,-5.49073 8.504941,-6.22406 1.311503,-0.45808 1.082114,-0.0564 -1.230931,2.1552 -5.969501,5.70781 -9.192268,13.33094 -8.843185,20.91763 0.06617,1.43808 0.754336,4.72819 1.529257,7.31135 1.846472,6.15507 4.822025,10.02093 10.028426,13.02896 3.679909,2.12609 10.391792,4.17245 14.045713,4.28235 2.904416,0.0873 1.991077,0.95266 -1.438758,1.36306 -4.235661,0.50684 -10.611638,-0.27603 -13.987114,-1.71736 z m 227.315214,-6.86409 c -12.9593,-5.55054 -26.49433,-21.17623 -37.78949,-43.62656 -7.81255,-15.52824 -13.92433,-34.93343 -16.89942,-53.65637 -4.65373,-29.287217 -0.021,-55.756011 11.22249,-64.119677 3.41438,-2.539848 6.8271,-3.326582 11.36347,-2.619659 5.70273,0.88871 10.79861,3.393489 16.26471,7.994698 20.18618,16.992055 37.65844,52.444868 43.65329,88.576438 4.79576,28.90469 1.35375,53.59402 -8.99418,64.51473 -4.57141,4.82444 -11.78497,5.94988 -18.82087,2.9364 z m 10.75217,-8.23769 c 4.72961,-2.4954 9.04559,-14.57075 9.82928,-27.50049 1.66532,-27.47598 -6.79816,-60.9981 -22.15037,-87.733113 -11.15521,-19.426171 -26.85746,-33.573667 -34.80569,-31.359448 -1.9649,0.547383 -4.2605,3.278778 -6.23361,7.416968 -2.2112,4.637547 -4.37964,15.057793 -4.66894,22.435815 -1.02077,26.035108 6.71141,57.007528 20.73595,83.061008 3.67721,6.83118 13.61496,20.57519 17.75709,24.55823 8.18456,7.87021 15.4648,11.26918 19.53629,9.12103 z"
id="path826"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

2
docs/docker.md

@ -6,7 +6,7 @@ The host services worker is required to be operating on each host. And, we tend
## Initialize the Environment
```sh
docker-compose run sites-host-services node dtp-sites-cli.js --action=create-domain shing.tv
docker-compose run sites-host-services node dtp-sites-cli.js --action=create-domain ourvoice.stream
```
## Grand Admin Access

2
dtp-sites.js

@ -29,7 +29,6 @@ module.config = {
module.log.info('registering Page service middleware');
const {
feed: feedService,
gabTV: gabTvService,
page: pageService,
siteLink: siteLinkService,
venue: venueService,
@ -39,7 +38,6 @@ module.config = {
pageService.menuMiddleware.bind(pageService),
siteLinkService.middleware(),
venueService.channelMiddleware(),
gabTvService.channelMiddleware()
);
},
};

2
static/502.html

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DTP Soapbox Community Engine</title>
<title>DTP Sites | Maintenance Mode</title>
<style type="text/css">
html, body {

29
static/offline.html

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shing.tv | Offline</title>
<style type="text/css">
html, body {
margin: 0;
padding: 0;
}
</style>
</style>
</head>
<body>
<div class="container">
<section>
<img src="/static/img/header.png" class="responsive" alt="Digital Telepresence Platform"/>
<p>Shing.tv as you know it has been closed for at least two weeks. Meeting less than 50% of the funding goal means this site won't be here for at least 50% of a month. Because we're taking advantage of this time to make some pretty radical changes to the site and the services provided.</p>
<p>This <a href="https://twitter.com/dtp_llc/status/1741661171294323022">thread on X</a> describes what's happening, and this is a fine time to follow us on X to keep up with the progress as we go.</p>
</section>
</div>
</body>
</html>
Loading…
Cancel
Save