feat: wire up paidreviews (#3656)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
download_url,
|
download_url,
|
||||||
@@ -777,6 +777,32 @@ class ExtensionDetailsRequest(BaseModel):
|
|||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionReviewsStatus(BaseModel):
|
||||||
|
tag: str
|
||||||
|
avg_rating: float
|
||||||
|
review_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class CreateExtensionReview(BaseModel):
|
||||||
|
tag: str
|
||||||
|
name: str | None = Field(None)
|
||||||
|
rating: int = Field(..., ge=0, le=1000)
|
||||||
|
comment: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionReviewPaymentRequest(BaseModel):
|
||||||
|
payment_hash: str
|
||||||
|
payment_request: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionReview(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str | None = Field(default=None)
|
||||||
|
tag: str | None = Field(default=None)
|
||||||
|
rating: int = Field(default=0, ge=0, le=1000)
|
||||||
|
comment: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
if settings.lnbits_ext_github_token:
|
if settings.lnbits_ext_github_token:
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import httpx
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.requests import Request
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_extensions
|
from lnbits.core.crud.extensions import get_user_extensions
|
||||||
@@ -14,10 +16,14 @@ from lnbits.core.models import (
|
|||||||
)
|
)
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
CreateExtension,
|
CreateExtension,
|
||||||
|
CreateExtensionReview,
|
||||||
Extension,
|
Extension,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionMeta,
|
ExtensionMeta,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
|
ExtensionReview,
|
||||||
|
ExtensionReviewPaymentRequest,
|
||||||
|
ExtensionReviewsStatus,
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
PayToEnableInfo,
|
PayToEnableInfo,
|
||||||
ReleasePaymentInfo,
|
ReleasePaymentInfo,
|
||||||
@@ -34,6 +40,7 @@ from lnbits.core.services.extensions import (
|
|||||||
install_extension,
|
install_extension,
|
||||||
uninstall_extension,
|
uninstall_extension,
|
||||||
)
|
)
|
||||||
|
from lnbits.db import Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
check_account_id_exists,
|
check_account_id_exists,
|
||||||
@@ -589,3 +596,49 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
for ext in installable_exts
|
for ext in installable_exts
|
||||||
]
|
]
|
||||||
return extension_data
|
return extension_data
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.get(
|
||||||
|
"/reviews/tags",
|
||||||
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
)
|
||||||
|
async def get_extension_reviews_tags() -> list[ExtensionReviewsStatus]:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.get(settings.lnbits_extensions_reviews_url + "/tags")
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return [ExtensionReviewsStatus(**item) for item in data]
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.get(
|
||||||
|
"/reviews/{ext_id}",
|
||||||
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
)
|
||||||
|
async def get_extension_reviews(ext_id: str, request: Request) -> Page[ExtensionReview]:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
query_string = str(request.query_params)
|
||||||
|
resp = await client.get(
|
||||||
|
settings.lnbits_extensions_reviews_url + f"/reviews/{ext_id}?{query_string}"
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
reviews = resp.json()
|
||||||
|
return Page(
|
||||||
|
data=[ExtensionReview(**item) for item in reviews["data"]],
|
||||||
|
total=reviews["total"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.put(
|
||||||
|
"/reviews",
|
||||||
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
)
|
||||||
|
async def create_extension_review(
|
||||||
|
data: CreateExtensionReview,
|
||||||
|
) -> ExtensionReviewPaymentRequest:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.post(
|
||||||
|
settings.lnbits_extensions_reviews_url + "/reviews", json=data.dict()
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
payment_request = resp.json()
|
||||||
|
return ExtensionReviewPaymentRequest(**payment_request)
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
|
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||||
"has_holdinvoice": settings.has_holdinvoice,
|
"has_holdinvoice": settings.has_holdinvoice,
|
||||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ class ExtensionsSettings(LNbitsSettings):
|
|||||||
lnbits_user_default_extensions: list[str] = Field(default=[])
|
lnbits_user_default_extensions: list[str] = Field(default=[])
|
||||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||||
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
||||||
|
lnbits_extensions_reviews_url: str = Field(
|
||||||
|
default="https://demo.lnbits.com/paidreviews/api/v1/AdFzLjzuKFLsdk4Bcnff6r",
|
||||||
|
description="""
|
||||||
|
URL for the paid reviews.
|
||||||
|
Regular users can view this URL (not secret).
|
||||||
|
""",
|
||||||
|
)
|
||||||
lnbits_extensions_manifests: list[str] = Field(
|
lnbits_extensions_manifests: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -187,6 +187,25 @@ window.localisation.en = {
|
|||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
|
reviews_url: 'Reviews URL',
|
||||||
|
reviews_url_label: 'Reviews server URL',
|
||||||
|
reviews_url_hint:
|
||||||
|
'Full PaidReviews URL including the settings id (e.g. https://example.com/paidreviews/SETTINGS_ID)',
|
||||||
|
reviews_open: 'View reviews',
|
||||||
|
reviews_leave: 'Leave a review',
|
||||||
|
reviews_name: 'Your name',
|
||||||
|
reviews_comment: 'Your review',
|
||||||
|
reviews_rating: 'Rating',
|
||||||
|
reviews_submit: 'Submit review',
|
||||||
|
reviews_loading: 'Loading reviews...',
|
||||||
|
reviews_refresh: 'Refresh reviews',
|
||||||
|
reviews_error_load: 'Could not load reviews',
|
||||||
|
reviews_url_not_configured: 'Reviews URL not configured',
|
||||||
|
reviews_pay_invoice: 'Pay invoice',
|
||||||
|
reviews_invoice_paid: 'Invoice paid',
|
||||||
|
reviews_invoice_title: 'Pay this invoice to submit your review',
|
||||||
|
reviews_count: 'Reviews',
|
||||||
|
no_reviews: 'No reviews yet',
|
||||||
extension_has_free_release: 'Has free releases',
|
extension_has_free_release: 'Has free releases',
|
||||||
extension_has_paid_release: 'Has paid releases',
|
extension_has_paid_release: 'Has paid releases',
|
||||||
extension_depends_on: 'Depends on:',
|
extension_depends_on: 'Depends on:',
|
||||||
|
|||||||
@@ -3,7 +3,34 @@ window.app.component(QrcodeVue)
|
|||||||
window.app.component('lnbits-extension-rating', {
|
window.app.component('lnbits-extension-rating', {
|
||||||
template: '#lnbits-extension-rating',
|
template: '#lnbits-extension-rating',
|
||||||
name: 'lnbits-extension-rating',
|
name: 'lnbits-extension-rating',
|
||||||
props: ['rating']
|
props: {
|
||||||
|
rating: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
clickable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayRating() {
|
||||||
|
return Math.round((this.rating || 0) * 2) / 2
|
||||||
|
},
|
||||||
|
hasData() {
|
||||||
|
return this.count !== null && this.count !== undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
if (!this.clickable) return
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.app.component('lnbits-fsat', {
|
window.app.component('lnbits-fsat', {
|
||||||
|
|||||||
@@ -27,7 +27,62 @@ window.PageExtensions = {
|
|||||||
uninstallAndDropDb: false,
|
uninstallAndDropDb: false,
|
||||||
maxStars: 5,
|
maxStars: 5,
|
||||||
paylinkWebsocket: null,
|
paylinkWebsocket: null,
|
||||||
searchToggle: false
|
searchToggle: false,
|
||||||
|
reviewsUrl: null,
|
||||||
|
reviewsDialog: {
|
||||||
|
show: false,
|
||||||
|
extension: null,
|
||||||
|
loading: false,
|
||||||
|
submitting: false,
|
||||||
|
form: {
|
||||||
|
name: '',
|
||||||
|
rating: 0,
|
||||||
|
comment: ''
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
reviews: [],
|
||||||
|
reviewsTable: {
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Name'),
|
||||||
|
field: 'name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'comment',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Comment'),
|
||||||
|
field: 'comment'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'created_at',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Date'),
|
||||||
|
field: 'created_at'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rating',
|
||||||
|
align: 'right',
|
||||||
|
label: 'Rating',
|
||||||
|
field: 'rating'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 5,
|
||||||
|
sortBy: 'created_at',
|
||||||
|
descending: true,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paymentDialog: {
|
||||||
|
show: false,
|
||||||
|
invoice: '',
|
||||||
|
hash: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -72,7 +127,6 @@ window.PageExtensions = {
|
|||||||
// the install logic has been triggered one way or another
|
// the install logic has been triggered one way or another
|
||||||
this.unsubscribeFromPaylinkWs()
|
this.unsubscribeFromPaylinkWs()
|
||||||
|
|
||||||
const extension = this.selectedExtension
|
|
||||||
this.selectedExtension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
this.showManageExtensionDialog = false
|
this.showManageExtensionDialog = false
|
||||||
release.payment_hash =
|
release.payment_hash =
|
||||||
@@ -286,8 +340,7 @@ window.PageExtensions = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v1/extension/${extension.id}/releases`,
|
`/api/v1/extension/${extension.id}/releases`
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
||||||
@@ -325,6 +378,9 @@ window.PageExtensions = {
|
|||||||
if (!detailsLink) {
|
if (!detailsLink) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// keep a reference to the extension so we can show rating stats
|
||||||
|
this.selectedExtension =
|
||||||
|
this.extensions.find(ext => ext.id === extId) || this.selectedExtension
|
||||||
this.selectedExtensionDetails = null
|
this.selectedExtensionDetails = null
|
||||||
this.showExtensionDetailsDialog = true
|
this.showExtensionDetailsDialog = true
|
||||||
this.slide = 0
|
this.slide = 0
|
||||||
@@ -333,8 +389,7 @@ window.PageExtensions = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
|
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.selectedExtensionDetails = data
|
this.selectedExtensionDetails = data
|
||||||
@@ -455,7 +510,7 @@ window.PageExtensions = {
|
|||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/v1/extension/${extId}/invoice/install`,
|
`/api/v1/extension/${extId}/invoice/install`,
|
||||||
this.g.user.wallets[0].adminkey,
|
null,
|
||||||
{
|
{
|
||||||
ext_id: extId,
|
ext_id: extId,
|
||||||
archive: release.archive,
|
archive: release.archive,
|
||||||
@@ -471,7 +526,7 @@ window.PageExtensions = {
|
|||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/v1/extension/${extId}/invoice/enable`,
|
`/api/v1/extension/${extId}/invoice/enable`,
|
||||||
this.g.user.wallets[0].adminkey,
|
null,
|
||||||
{
|
{
|
||||||
amount
|
amount
|
||||||
}
|
}
|
||||||
@@ -562,8 +617,7 @@ window.PageExtensions = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
|
`/api/v1/extension/release/${org}/${repo}/${release.version}`
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
)
|
||||||
release.loaded = true
|
release.loaded = true
|
||||||
release.is_version_compatible = data.is_version_compatible
|
release.is_version_compatible = data.is_version_compatible
|
||||||
@@ -588,18 +642,13 @@ window.PageExtensions = {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ext.inProgress = true
|
ext.inProgress = true
|
||||||
await LNbits.api.request(
|
await LNbits.api.request('POST', `/api/v1/extension`, null, {
|
||||||
'POST',
|
ext_id: ext.id,
|
||||||
`/api/v1/extension`,
|
archive: ext.latestRelease.archive,
|
||||||
this.g.user.wallets[0].adminkey,
|
source_repo: ext.latestRelease.source_repo,
|
||||||
{
|
payment_hash: ext.latestRelease.payment_hash,
|
||||||
ext_id: ext.id,
|
version: ext.latestRelease.version
|
||||||
archive: ext.latestRelease.archive,
|
})
|
||||||
source_repo: ext.latestRelease.source_repo,
|
|
||||||
payment_hash: ext.latestRelease.payment_hash,
|
|
||||||
version: ext.latestRelease.version
|
|
||||||
}
|
|
||||||
)
|
|
||||||
count++
|
count++
|
||||||
ext.isAvailable = true
|
ext.isAvailable = true
|
||||||
ext.isInstalled = true
|
ext.isInstalled = true
|
||||||
@@ -624,6 +673,163 @@ window.PageExtensions = {
|
|||||||
})
|
})
|
||||||
this.showUpdateAllDialog = false
|
this.showUpdateAllDialog = false
|
||||||
},
|
},
|
||||||
|
formatAvg(raw) {
|
||||||
|
const val = Number(raw || 0)
|
||||||
|
return Math.round((val / 2 / 100) * 2) / 2
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadReviewStats() {
|
||||||
|
if (!this.reviewsUrl) {
|
||||||
|
console.info('Extension reviews are not configured')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/extension/reviews/tags`
|
||||||
|
)
|
||||||
|
const map = {}
|
||||||
|
data.forEach(stat => {
|
||||||
|
map[stat.tag] = stat
|
||||||
|
})
|
||||||
|
this.extensions.forEach(ext => {
|
||||||
|
ext.reviewStats = map[ext.id] || null
|
||||||
|
})
|
||||||
|
this.filterExtensions(this.searchTerm, this.tab)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openReviews(extension) {
|
||||||
|
const targetExtension =
|
||||||
|
extension ||
|
||||||
|
(this.selectedExtensionDetails
|
||||||
|
? this.extensions.find(e => e.id === this.selectedExtensionDetails.id)
|
||||||
|
: null)
|
||||||
|
if (!targetExtension) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.reviewsUrl) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: this.$t('reviews_url_not_configured')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.reviewsDialog.extension = targetExtension
|
||||||
|
this.selectedExtension = extension
|
||||||
|
this.reviewsDialog.show = true
|
||||||
|
await this.getTagReviews()
|
||||||
|
},
|
||||||
|
async getTagReviews(props) {
|
||||||
|
if (!this.reviewsUrl) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: this.$t('reviews_url_not_configured')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.reviewsTable.loading = true
|
||||||
|
const params = LNbits.utils.prepareFilterQuery(this.reviewsTable, props)
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/extension/reviews/${this.selectedExtension.id}?${params}`
|
||||||
|
)
|
||||||
|
this.reviews = data.data
|
||||||
|
this.reviewsTable.pagination.rowsNumber = data.total
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
} finally {
|
||||||
|
this.reviewsTable.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatReviewDate(val) {
|
||||||
|
if (!val) return ''
|
||||||
|
const numeric = Number(val)
|
||||||
|
if (!Number.isNaN(numeric)) {
|
||||||
|
return this.utils.formatTimestamp(numeric)
|
||||||
|
}
|
||||||
|
return this.utils.formatDate(val)
|
||||||
|
},
|
||||||
|
async submitReview() {
|
||||||
|
if (!this.reviewsDialog.extension || !this.reviewsUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.reviewsDialog.submitting = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
tag: this.reviewsDialog.extension.id,
|
||||||
|
name: this.reviewsDialog.form.name,
|
||||||
|
rating: this.reviewsDialog.form.rating * 100,
|
||||||
|
comment: this.reviewsDialog.form.comment
|
||||||
|
}
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/reviews`,
|
||||||
|
null,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
if (data.payment_request) {
|
||||||
|
this.openInvoiceDialog(data.payment_request, data.payment_hash)
|
||||||
|
} else {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Review submitted'
|
||||||
|
})
|
||||||
|
this.resetReviewForm()
|
||||||
|
await this.getTagReviews()
|
||||||
|
await this.loadReviewStats()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
} finally {
|
||||||
|
this.reviewsDialog.submitting = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openInvoiceDialog(invoice, hash) {
|
||||||
|
this.paymentDialog.invoice = invoice
|
||||||
|
this.paymentDialog.hash = hash
|
||||||
|
this.paymentDialog.show = true
|
||||||
|
this.listenForPayment(hash)
|
||||||
|
},
|
||||||
|
resetReviewForm() {
|
||||||
|
this.reviewsDialog.form = {
|
||||||
|
name: '',
|
||||||
|
rating: 0,
|
||||||
|
comment: ''
|
||||||
|
}
|
||||||
|
this.paymentDialog = {
|
||||||
|
show: false,
|
||||||
|
invoice: '',
|
||||||
|
hash: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listenForPayment(hash) {
|
||||||
|
try {
|
||||||
|
const base = new URL(this.reviewsUrl)
|
||||||
|
base.protocol = base.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
base.pathname = `/api/v1/ws/${hash}`
|
||||||
|
const ws = new WebSocket(base)
|
||||||
|
ws.addEventListener('message', async () => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: this.$t('reviews_invoice_paid')
|
||||||
|
})
|
||||||
|
this.paymentDialog.show = false
|
||||||
|
this.resetReviewForm()
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.getTagReviews()
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
await this.loadReviewStats()
|
||||||
|
ws.close()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
async fetchAllExtensions() {
|
async fetchAllExtensions() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||||
@@ -638,6 +844,7 @@ window.PageExtensions = {
|
|||||||
async created() {
|
async created() {
|
||||||
this.extensions = await this.fetchAllExtensions()
|
this.extensions = await this.fetchAllExtensions()
|
||||||
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||||
|
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
|
||||||
|
|
||||||
if (this.g.user.extensions.length === 0) {
|
if (this.g.user.extensions.length === 0) {
|
||||||
this.tab = 'all'
|
this.tab = 'all'
|
||||||
@@ -654,6 +861,8 @@ window.PageExtensions = {
|
|||||||
this.hasNewVersion(ext)
|
this.hasNewVersion(ext)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await this.loadReviewStats()
|
||||||
|
|
||||||
this.filterExtensions(this.searchTerm, this.tab)
|
this.filterExtensions(this.searchTerm, this.tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -561,11 +561,32 @@ include('components/lnbits-error.vue') %}
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="lnbits-extension-rating">
|
<template id="lnbits-extension-rating">
|
||||||
<div style="margin-bottom: 3px">
|
<div class="row items-center q-gutter-xs" style="margin-bottom: 3px">
|
||||||
<q-rating v-model="rating" size="1.5em" :max="5" color="primary"
|
<q-rating
|
||||||
><q-tooltip>
|
:model-value="displayRating"
|
||||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
size="1.4em"
|
||||||
></q-rating>
|
:max="5"
|
||||||
|
color="primary"
|
||||||
|
icon="star_border"
|
||||||
|
icon-selected="star"
|
||||||
|
icon-half="star_half"
|
||||||
|
readonly
|
||||||
|
:class="clickable ? 'cursor-pointer' : ''"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!hasData">
|
||||||
|
<span v-text="$t('extension_rating_soon')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
<q-tooltip v-else-if="clickable">
|
||||||
|
<span v-text="$t('reviews_open')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-rating>
|
||||||
|
<div
|
||||||
|
class="text-caption text-grey"
|
||||||
|
v-if="count !== null && count !== undefined"
|
||||||
|
>
|
||||||
|
<span v-text="`(${count})`"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,19 @@
|
|||||||
:hint="$t('extension_builder_manifest_url_hint')"
|
:hint="$t('extension_builder_manifest_url_hint')"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('reviews_url')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_extensions_reviews_url"
|
||||||
|
:label="$t('reviews_url_label')"
|
||||||
|
:hint="$t('reviews_url_hint')"
|
||||||
|
type="url"
|
||||||
|
autocomplete="off"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|||||||
@@ -163,7 +163,22 @@
|
|||||||
v-text="extension.name"
|
v-text="extension.name"
|
||||||
></div>
|
></div>
|
||||||
<div style="justify-content: space-between; display: flex">
|
<div style="justify-content: space-between; display: flex">
|
||||||
<lnbits-extension-rating :rating="0" />
|
<lnbits-extension-rating
|
||||||
|
:rating="
|
||||||
|
formatAvg(
|
||||||
|
extension.reviewStats
|
||||||
|
? extension.reviewStats.avg_rating
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:count="
|
||||||
|
extension.reviewStats
|
||||||
|
? extension.reviewStats.review_count
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:clickable="!!reviewsUrl"
|
||||||
|
@click="openReviews(extension)"
|
||||||
|
/>
|
||||||
<q-btn-group size="xs" style="margin: 5px 0">
|
<q-btn-group size="xs" style="margin: 5px 0">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="extension.hasFreeRelease"
|
v-if="extension.hasFreeRelease"
|
||||||
@@ -998,8 +1013,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-md-4 q-pl-sm">
|
<div class="col-sm-12 col-md-4 q-pl-sm">
|
||||||
<lnbits-extension-rating
|
<lnbits-extension-rating
|
||||||
:rating="0"
|
:rating="
|
||||||
size="2.5em"
|
formatAvg(
|
||||||
|
selectedExtension && selectedExtension.reviewStats
|
||||||
|
? selectedExtension.reviewStats.avg_rating
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:count="
|
||||||
|
selectedExtension && selectedExtension.reviewStats
|
||||||
|
? selectedExtension.reviewStats.review_count
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:clickable="!!reviewsUrl"
|
||||||
|
@click="openReviews(selectedExtension)"
|
||||||
></lnbits-extension-rating>
|
></lnbits-extension-rating>
|
||||||
<div class="q-mt-md">
|
<div class="q-mt-md">
|
||||||
<b>
|
<b>
|
||||||
@@ -1057,6 +1084,182 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
<q-dialog v-model="reviewsDialog.show" position="top">
|
||||||
|
<q-card
|
||||||
|
class="q-pa-md lnbits__dialog-card"
|
||||||
|
style="width: 900px; max-width: 95vw"
|
||||||
|
>
|
||||||
|
<q-card-section class="q-gutter-y-sm">
|
||||||
|
<div class="row items-center">
|
||||||
|
<div class="col">
|
||||||
|
<div
|
||||||
|
class="text-h6"
|
||||||
|
v-text="
|
||||||
|
(reviewsDialog.extension && reviewsDialog.extension.name) ||
|
||||||
|
(reviewsDialog.extension && reviewsDialog.extension.id)
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="text-caption text-grey"
|
||||||
|
v-text="reviewsDialog.extension && reviewsDialog.extension.id"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<lnbits-extension-rating
|
||||||
|
:rating="
|
||||||
|
formatAvg(
|
||||||
|
reviewsDialog.extension && reviewsDialog.extension.reviewStats
|
||||||
|
? reviewsDialog.extension.reviewStats.avg_rating
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:count="
|
||||||
|
reviewsDialog.extension && reviewsDialog.extension.reviewStats
|
||||||
|
? reviewsDialog.extension.reviewStats.review_count
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
></lnbits-extension-rating>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-card-section v-if="!reviewsUrl">
|
||||||
|
<div
|
||||||
|
class="text-negative"
|
||||||
|
v-text="$t('reviews_url_not_configured')"
|
||||||
|
></div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-else>
|
||||||
|
<div v-if="reviewsDialog.loading" class="row justify-center q-pa-lg">
|
||||||
|
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="reviewsDialog.error" class="text-negative q-mb-md">
|
||||||
|
<span v-text="reviewsDialog.error"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-form @submit="submitReview" class="q-gutter-md">
|
||||||
|
<div class="row q-col-gutter-lg">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="reviewsDialog.form.name"
|
||||||
|
:label="$t('reviews_name')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-rating
|
||||||
|
v-model="reviewsDialog.form.rating"
|
||||||
|
max="10"
|
||||||
|
icon="star_border"
|
||||||
|
icon-selected="star"
|
||||||
|
icon-half="star_half"
|
||||||
|
color="primary"
|
||||||
|
class="float-right"
|
||||||
|
></q-rating>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
autogrow
|
||||||
|
v-model="reviewsDialog.form.comment"
|
||||||
|
:label="$t('reviews_comment')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
class="float-right"
|
||||||
|
unelevated
|
||||||
|
:loading="reviewsDialog.submitting"
|
||||||
|
:disable="!reviewsDialog.form.rating"
|
||||||
|
:label="$t('reviews_submit')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
<q-separator class="q-my-md"></q-separator>
|
||||||
|
<div class="row q-col-gutter-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<div v-if="reviews.length === 0" class="text-grey q-mb-md">
|
||||||
|
<span v-text="$t('no_reviews')"></span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<q-table
|
||||||
|
:rows="reviews"
|
||||||
|
:columns="reviewsTable.columns"
|
||||||
|
v-model:pagination="reviewsTable.pagination"
|
||||||
|
@request="getTagReviews"
|
||||||
|
:loading="reviewsTable.loading"
|
||||||
|
row-key="name"
|
||||||
|
:filter="reviewsTable.search"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<span v-text="props.row.name"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="comment" :props="props">
|
||||||
|
<span v-text="props.row.comment"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="created_at" :props="props">
|
||||||
|
<span
|
||||||
|
v-text="formatReviewDate(props.row.created_at)"
|
||||||
|
></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="rating" :props="props">
|
||||||
|
<q-rating
|
||||||
|
class="float-right q-pt-xs"
|
||||||
|
readonly
|
||||||
|
icon="star_border"
|
||||||
|
icon-selected="star"
|
||||||
|
icon-half="star_half"
|
||||||
|
:model-value="formatAvg(props.row.rating)"
|
||||||
|
size="sm"
|
||||||
|
color="secondary"
|
||||||
|
></q-rating>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator v-if="reviews.length"></q-separator>
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn
|
||||||
|
@click="reviewsDialog.show = false"
|
||||||
|
color="grey"
|
||||||
|
outline
|
||||||
|
class="float-right"
|
||||||
|
:label="$t('close')"
|
||||||
|
></q-btn>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
<q-dialog v-model="paymentDialog.show" position="top">
|
||||||
|
<q-card class="q-pa-md lnbits__dialog-card">
|
||||||
|
<q-card-section>
|
||||||
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="paymentDialog.invoice"
|
||||||
|
:options="{width: 800}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="between">
|
||||||
|
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
<q-dialog v-model="showUpdateAllDialog" position="top">
|
<q-dialog v-model="showUpdateAllDialog" position="top">
|
||||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
Reference in New Issue
Block a user