mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 20:49:02 +00:00
Work on sharing
This commit is contained in:
@@ -126,7 +126,9 @@ const Query = {
|
||||
|
||||
const PhotoURL = {
|
||||
url(root, args, ctx, info) {
|
||||
return new URL(root.url, config.host).href
|
||||
let url = new URL(root.url, config.host)
|
||||
if (ctx.shareToken) url.search = `?token=${ctx.shareToken}`
|
||||
return url.href
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const Mutation = {
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (a:Album { id: {albumId} })
|
||||
CREATE (share:ShareToken {shareToken} )
|
||||
CREATE (u)-[:SHARE_TOKEN]->(share)<-[:SHARES]-(a)
|
||||
CREATE (u)-[:SHARE_TOKEN]->(share)-[:SHARES]->(a)
|
||||
RETURN share
|
||||
`,
|
||||
{
|
||||
@@ -73,7 +73,7 @@ const Mutation = {
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (p:Photo { id: {photoId} })
|
||||
CREATE (share:ShareToken {shareToken} )
|
||||
CREATE (u)-[:SHARE_TOKEN]->(share)<-[:SHARES]-(p)
|
||||
CREATE (u)-[:SHARE_TOKEN]->(share)-[:SHARES]->(p)
|
||||
RETURN share
|
||||
`,
|
||||
{
|
||||
@@ -125,7 +125,7 @@ const Query = {
|
||||
`
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (u)-[:OWNS]->(a:Album { id: {albumId} })
|
||||
MATCH (a)-[:SHARES]->(shareToken:ShareToken)
|
||||
MATCH (a)<-[:SHARES]-(shareToken:ShareToken)
|
||||
`
|
||||
)
|
||||
|
||||
@@ -149,7 +149,7 @@ const Query = {
|
||||
`
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (u)-[:OWNS]->(a:Album)-[:CONTAINS]->(p:Photo {id: {photoId} })
|
||||
MATCH (p)-[:SHARES]->(shareToken:ShareToken)
|
||||
MATCH (p)<-[:SHARES]-(shareToken:ShareToken)
|
||||
`
|
||||
)
|
||||
|
||||
@@ -167,6 +167,10 @@ const Query = {
|
||||
|
||||
return tokens
|
||||
},
|
||||
shareToken(root, args, ctx, info) {
|
||||
ctx.shareToken = args.token
|
||||
return neo4jgraphql(root, args, ctx, info)
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -5,6 +5,13 @@ import config from '../config'
|
||||
import { isRawImage, getImageCachePath } from '../scanner/utils'
|
||||
import { getUserFromToken, getTokenFromBearer } from '../token'
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(httpCode, message) {
|
||||
super(message)
|
||||
this.httpCode = httpCode
|
||||
}
|
||||
}
|
||||
|
||||
async function sendImage({ photo, res, id, albumId, image, scanner }) {
|
||||
let imagePath = path.resolve(getImageCachePath(id, albumId), image)
|
||||
|
||||
@@ -39,48 +46,129 @@ async function sendImage({ photo, res, id, albumId, image, scanner }) {
|
||||
res.sendFile(imagePath)
|
||||
}
|
||||
|
||||
async function verifyUser({ req, driver, id }) {
|
||||
let user = null
|
||||
|
||||
try {
|
||||
const token = getTokenFromBearer(req.headers.authorization)
|
||||
user = await getUserFromToken(token, driver)
|
||||
} catch (err) {
|
||||
throw new RequestError(401, err.message)
|
||||
// return res.status(401).send(err.message)
|
||||
}
|
||||
|
||||
const session = driver.session()
|
||||
|
||||
const result = await session.run(
|
||||
'MATCH (p:Photo { id: {id} })<-[:CONTAINS]-(a:Album)<-[:OWNS]-(u:User) RETURN p as photo, u.id as userId, a.id as albumId',
|
||||
{
|
||||
id,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (result.records.length == 0) {
|
||||
throw new RequestError(404, 'Image not found')
|
||||
// return res.status(404).send(`Image not found`)
|
||||
}
|
||||
|
||||
const userId = result.records[0].get('userId')
|
||||
const albumId = result.records[0].get('albumId')
|
||||
const photo = result.records[0].get('photo').properties
|
||||
|
||||
if (userId != user.id) {
|
||||
throw new RequestError(401, 'Image not owned by you')
|
||||
// return res.status(401).send(`Image not owned by you`)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
albumId,
|
||||
photo,
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyShareToken({ shareToken, id, driver }) {
|
||||
const session = driver.session()
|
||||
|
||||
const shareTokenResult = await session.run(
|
||||
`MATCH (share:ShareToken { token: {shareToken} })-[:SHARES]->(shared)
|
||||
MATCH (photo:Photo { id: {id} })<-[:CONTAINS]-(album:Album)
|
||||
RETURN share, photo, shared, album`,
|
||||
{ shareToken, id }
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (shareTokenResult.records.length == 0) {
|
||||
throw new RequestError(404, 'Image not found')
|
||||
}
|
||||
|
||||
const share = shareTokenResult.records[0].get('share').properties
|
||||
const album = shareTokenResult.records[0].get('album').properties
|
||||
const photo = shareTokenResult.records[0].get('photo').properties
|
||||
const sharedObject = shareTokenResult.records[0].get('shared')
|
||||
|
||||
if (sharedObject.labels[0] == 'Album') {
|
||||
const session = driver.session()
|
||||
const albumResult = await session.run(
|
||||
`MATCH (album)-[:CONTAINS]->(photo:Photo { id: {id} })
|
||||
RETURN album`,
|
||||
{ id }
|
||||
)
|
||||
session.close()
|
||||
|
||||
if (albumResult.records.length == 0) {
|
||||
throw new RequestError(403, 'Invalid share token')
|
||||
}
|
||||
} else {
|
||||
const sharedPhoto = sharedObject.properties
|
||||
|
||||
if (sharedPhoto.id != photo.id) {
|
||||
throw new RequestError(403, 'Invalid share token')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
photo,
|
||||
albumId: album.id,
|
||||
}
|
||||
}
|
||||
|
||||
function loadImageRoutes({ app, driver, scanner }) {
|
||||
app.use('/images/:id/:image', async (req, res) => {
|
||||
const { id, image } = req.params
|
||||
|
||||
let user = null
|
||||
const shareToken = req.query.token
|
||||
|
||||
let photo, albumId
|
||||
|
||||
try {
|
||||
const token = getTokenFromBearer(req.headers.authorization)
|
||||
user = await getUserFromToken(token, driver)
|
||||
} catch (err) {
|
||||
return res.status(401).send(err.message)
|
||||
}
|
||||
let verify = null
|
||||
|
||||
const session = driver.session()
|
||||
|
||||
const result = await session.run(
|
||||
'MATCH (p:Photo { id: {id} })<-[:CONTAINS]-(a:Album)<-[:OWNS]-(u:User) RETURN p as photo, u.id as userId, a.id as albumId',
|
||||
{
|
||||
id,
|
||||
if (shareToken) {
|
||||
verify = await verifyShareToken({ shareToken, id, driver })
|
||||
}
|
||||
)
|
||||
|
||||
if (result.records.length == 0) {
|
||||
return res.status(404).send(`Image not found`)
|
||||
if (!verify) {
|
||||
verify = await verifyUser({
|
||||
req,
|
||||
driver,
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
if (verify == null) throw RequestError(500, 'Unable to verify request')
|
||||
|
||||
photo = verify.photo
|
||||
albumId = verify.albumId
|
||||
} catch (error) {
|
||||
return res.status(error.status || 500).send(error.message)
|
||||
}
|
||||
|
||||
const userId = result.records[0].get('userId')
|
||||
const albumId = result.records[0].get('albumId')
|
||||
const photo = result.records[0].get('photo').properties
|
||||
|
||||
if (userId != user.id) {
|
||||
return res.status(401).send(`Image not owned by you`)
|
||||
}
|
||||
|
||||
session.close()
|
||||
|
||||
sendImage({ photo, res, id, albumId, image, scanner })
|
||||
})
|
||||
|
||||
// app.use('/share/:token/:image', async (req, res) => {
|
||||
// const { token } = req.params
|
||||
// })
|
||||
}
|
||||
|
||||
export default loadImageRoutes
|
||||
|
||||
@@ -22,7 +22,7 @@ type Album {
|
||||
owner: User! @relation(name: "OWNS", direction: "IN")
|
||||
path: String
|
||||
|
||||
shares: [ShareToken] @relation(name: "SHARES", direction: "OUT")
|
||||
shares: [ShareToken] @relation(name: "SHARES", direction: "IN")
|
||||
}
|
||||
|
||||
type PhotoURL {
|
||||
@@ -61,7 +61,7 @@ type Photo {
|
||||
album: Album! @relation(name: "CONTAINS", direction: "IN")
|
||||
exif: PhotoEXIF @relation(name: "EXIF", direction: "OUT")
|
||||
|
||||
shares: [ShareToken] @relation(name: "SHARES", direction: "OUT")
|
||||
shares: [ShareToken] @relation(name: "SHARES", direction: "IN")
|
||||
}
|
||||
|
||||
type ShareToken {
|
||||
@@ -70,10 +70,10 @@ type ShareToken {
|
||||
# Optional expire date
|
||||
expire: Date
|
||||
# Optional password
|
||||
password: String
|
||||
# password: String
|
||||
|
||||
album: Album @relation(name: "SHARES", direction: "IN")
|
||||
photo: Photo @relation(name: "SHARES", direction: "IN")
|
||||
album: Album @relation(name: "SHARES", direction: "OUT")
|
||||
photo: Photo @relation(name: "SHARES", direction: "OUT")
|
||||
}
|
||||
|
||||
type SiteInfo {
|
||||
@@ -152,6 +152,8 @@ type Query {
|
||||
myPhotos: [Photo] @isAuthenticated
|
||||
photo(id: ID!): Photo @isAuthenticated
|
||||
|
||||
shareToken(token: ID!): ShareToken
|
||||
|
||||
albumShares(id: ID!, password: String): [ShareToken] @isAuthenticated
|
||||
photoShares(id: ID!, password: String): [ShareToken] @isAuthenticated
|
||||
}
|
||||
|
||||
13
ui/package-lock.json
generated
13
ui/package-lock.json
generated
@@ -2196,6 +2196,14 @@
|
||||
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
|
||||
},
|
||||
"copy-to-clipboard": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz",
|
||||
"integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==",
|
||||
"requires": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
|
||||
@@ -8565,6 +8573,11 @@
|
||||
"repeat-string": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"apollo-link-http": "^1.5.15",
|
||||
"apollo-link-ws": "^1.0.18",
|
||||
"babel-plugin-styled-components": "^1.10.6",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"graphql": "^14.2.1",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
|
||||
@@ -12,6 +12,12 @@ const adminQuery = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const Authorized = ({ children }) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
return token ? children : null
|
||||
}
|
||||
|
||||
const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Icon } from 'semantic-ui-react'
|
||||
import Sidebar from './components/sidebar/Sidebar'
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import { Authorized } from './AuthorizedRoute'
|
||||
|
||||
const adminQuery = gql`
|
||||
query adminQuery {
|
||||
@@ -93,30 +94,32 @@ class Layout extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
<SideMenu>
|
||||
<SideButton to="/photos" exact>
|
||||
<Icon name="image outline" />
|
||||
<SideButtonLabel>Photos</SideButtonLabel>
|
||||
</SideButton>
|
||||
<SideButton to="/albums" exact>
|
||||
<Icon name="images outline" />
|
||||
<SideButtonLabel>Albums</SideButtonLabel>
|
||||
</SideButton>
|
||||
<Query query={adminQuery}>
|
||||
{({ loading, error, data }) => {
|
||||
if (data && data.myUser && data.myUser.admin) {
|
||||
return (
|
||||
<SideButton to="/settings" exact>
|
||||
<Icon name="settings" />
|
||||
<SideButtonLabel>Settings</SideButtonLabel>
|
||||
</SideButton>
|
||||
)
|
||||
}
|
||||
<Authorized>
|
||||
<SideMenu>
|
||||
<SideButton to="/photos" exact>
|
||||
<Icon name="image outline" />
|
||||
<SideButtonLabel>Photos</SideButtonLabel>
|
||||
</SideButton>
|
||||
<SideButton to="/albums" exact>
|
||||
<Icon name="images outline" />
|
||||
<SideButtonLabel>Albums</SideButtonLabel>
|
||||
</SideButton>
|
||||
<Query query={adminQuery}>
|
||||
{({ loading, error, data }) => {
|
||||
if (data && data.myUser && data.myUser.admin) {
|
||||
return (
|
||||
<SideButton to="/settings" exact>
|
||||
<Icon name="settings" />
|
||||
<SideButtonLabel>Settings</SideButtonLabel>
|
||||
</SideButton>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}}
|
||||
</Query>
|
||||
</SideMenu>
|
||||
return null
|
||||
}}
|
||||
</Query>
|
||||
</SideMenu>
|
||||
</Authorized>
|
||||
<Sidebar>
|
||||
<Content>{this.props.children}</Content>
|
||||
</Sidebar>
|
||||
@@ -129,7 +132,7 @@ class Layout extends Component {
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
children: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
||||
42
ui/src/Pages/SharePage/AlbumSharePage.js
Normal file
42
ui/src/Pages/SharePage/AlbumSharePage.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import RouterPropTypes from 'react-router-prop-types'
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import Layout from '../../Layout'
|
||||
import PhotoGallery from '../../components/photoGallery/PhotoGallery'
|
||||
|
||||
const AlbumSharePage = ({ album }) => {
|
||||
const [activeIndex, setActiveIndex] = useState(-1)
|
||||
const [presenting, setPresenting] = useState(false)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>{album.title}</h1>
|
||||
<PhotoGallery
|
||||
photos={album.photos}
|
||||
loading={false}
|
||||
activeIndex={activeIndex}
|
||||
presenting={presenting}
|
||||
onSelectImage={index => {
|
||||
setActiveIndex(index)
|
||||
}}
|
||||
setPresenting={setPresenting}
|
||||
nextImage={() => {
|
||||
setActiveIndex((activeIndex + 1) % album.photos.length)
|
||||
}}
|
||||
previousImage={() => {
|
||||
setActiveIndex(
|
||||
activeIndex < 1 ? album.photos.length - 1 : activeIndex - 1
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
AlbumSharePage.propTypes = {
|
||||
album: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default AlbumSharePage
|
||||
39
ui/src/Pages/SharePage/PhotoSharePage.js
Normal file
39
ui/src/Pages/SharePage/PhotoSharePage.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import Layout from '../../Layout'
|
||||
import ProtectedImage from '../../components/photoGallery/ProtectedImage'
|
||||
import { SidebarConsumer } from '../../components/sidebar/Sidebar'
|
||||
import PhotoSidebar from '../../components/sidebar/PhotoSidebar'
|
||||
|
||||
const DisplayPhoto = styled(ProtectedImage)`
|
||||
width: 100%;
|
||||
max-height: calc(80vh);
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const AlbumSharePage = ({ photo }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<SidebarConsumer>
|
||||
{({ updateSidebar }) => (
|
||||
<>
|
||||
<h1>{photo.title}</h1>
|
||||
<DisplayPhoto
|
||||
src={photo.original.url}
|
||||
onLoad={() => {
|
||||
updateSidebar(<PhotoSidebar photo={photo} hidePreview />)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SidebarConsumer>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
AlbumSharePage.propTypes = {
|
||||
photo: PropTypes.object,
|
||||
}
|
||||
|
||||
export default AlbumSharePage
|
||||
86
ui/src/Pages/SharePage/SharePage.js
Normal file
86
ui/src/Pages/SharePage/SharePage.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react'
|
||||
import RouterProps from 'react-router-prop-types'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
import AlbumSharePage from './AlbumSharePage'
|
||||
import PhotoSharePage from './PhotoSharePage'
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const tokenQuery = gql`
|
||||
query SharePageToken($token: ID!) {
|
||||
shareToken(token: $token) {
|
||||
token
|
||||
album {
|
||||
id
|
||||
title
|
||||
photos(orderBy: title_desc) {
|
||||
...PhotoProps
|
||||
}
|
||||
}
|
||||
photo {
|
||||
...PhotoProps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment PhotoProps on Photo {
|
||||
id
|
||||
title
|
||||
thumbnail {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
original {
|
||||
url
|
||||
}
|
||||
exif {
|
||||
camera
|
||||
maker
|
||||
lens
|
||||
dateShot {
|
||||
formatted
|
||||
}
|
||||
fileSize
|
||||
exposure
|
||||
aperture
|
||||
iso
|
||||
focalLength
|
||||
flash
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SharePage = ({ match }) => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/:token`}>
|
||||
{({ match }) => (
|
||||
<Query query={tokenQuery} variables={{ token: match.params.token }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (error) return error.message
|
||||
if (loading) return 'Loading...'
|
||||
|
||||
if (data.shareToken.album) {
|
||||
return <AlbumSharePage album={data.shareToken.album} />
|
||||
}
|
||||
|
||||
if (data.shareToken.photo) {
|
||||
return <PhotoSharePage photo={data.shareToken.photo} />
|
||||
}
|
||||
|
||||
return <h1>Share not found</h1>
|
||||
}}
|
||||
</Query>
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/">Share not found</Route>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
SharePage.propTypes = {
|
||||
...RouterProps,
|
||||
}
|
||||
|
||||
export default SharePage
|
||||
@@ -8,6 +8,7 @@ const AlbumsPage = React.lazy(() => import('./Pages/AllAlbumsPage/AlbumsPage'))
|
||||
const AlbumPage = React.lazy(() => import('./Pages/AlbumPage/AlbumPage'))
|
||||
const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
|
||||
const PhotosPage = React.lazy(() => import('./Pages/PhotosPage/PhotosPage'))
|
||||
const SharePage = React.lazy(() => import('./Pages/SharePage/SharePage'))
|
||||
|
||||
const LoginPage = React.lazy(() => import('./Pages/LoginPage/LoginPage'))
|
||||
const InitialSetupPage = React.lazy(() =>
|
||||
@@ -31,6 +32,7 @@ class Routes extends React.Component {
|
||||
<Switch>
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/initialSetup" component={InitialSetupPage} />
|
||||
<Route path="/share" component={SharePage} />
|
||||
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
||||
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
||||
<AuthorizedRoute path="/photos" component={PhotosPage} />
|
||||
|
||||
@@ -88,7 +88,7 @@ const PhotoGallery = ({
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onSelectImage={index => {
|
||||
updateSidebar(<PhotoSidebar imageId={photo.id} />)
|
||||
updateSidebar(<PhotoSidebar photo={photo} />)
|
||||
onSelectImage(index)
|
||||
}}
|
||||
setPresenting={setPresenting}
|
||||
@@ -142,10 +142,10 @@ const PhotoGallery = ({
|
||||
) : (
|
||||
<PresentContainer>
|
||||
<Transition {...presentViewTransitionConfig}>
|
||||
{item => props => (
|
||||
{photo => props => (
|
||||
<PresentPhoto
|
||||
thumbnail={item && item.thumbnail.url}
|
||||
photoId={item && item.id}
|
||||
thumbnail={photo && photo.thumbnail.url}
|
||||
photo={photo}
|
||||
style={props}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -61,41 +61,55 @@ export const PresentPhoto = ({
|
||||
photo,
|
||||
thumbnail,
|
||||
imageLoaded,
|
||||
photoId,
|
||||
...otherProps
|
||||
}) => {
|
||||
let [originalPhoto, setOriginalPhoto] = useState(null)
|
||||
useEffect(() => {
|
||||
if (!photoId) return
|
||||
if (!(photo && photo.id)) return
|
||||
|
||||
function loadOriginalPhoto() {
|
||||
const originalPhoto = (
|
||||
<Query query={imageQuery} variables={{ id: photoId }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (error) {
|
||||
alert(error)
|
||||
let originalPhoto = null
|
||||
|
||||
if (photo && photo.original && photo.original.url) {
|
||||
originalPhoto = (
|
||||
<StyledPhoto
|
||||
style={{ display: 'none' }}
|
||||
src={photo.original.url}
|
||||
onLoad={e => {
|
||||
e.target.style.display = 'initial'
|
||||
imageLoaded && imageLoaded()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
originalPhoto = (
|
||||
<Query query={imageQuery} variables={{ id: photo.id }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (error) {
|
||||
alert(error)
|
||||
return null
|
||||
}
|
||||
|
||||
if (data && data.photo) {
|
||||
const photo = data.photo
|
||||
|
||||
return (
|
||||
<StyledPhoto
|
||||
style={{ display: 'none' }}
|
||||
src={photo.original.url}
|
||||
onLoad={e => {
|
||||
e.target.style.display = 'initial'
|
||||
imageLoaded && imageLoaded()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (data && data.photo) {
|
||||
const photo = data.photo
|
||||
|
||||
return (
|
||||
<StyledPhoto
|
||||
style={{ display: 'none' }}
|
||||
src={photo.original.url}
|
||||
onLoad={e => {
|
||||
e.target.style.display = 'initial'
|
||||
imageLoaded && imageLoaded()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
}
|
||||
|
||||
setOriginalPhoto(originalPhoto)
|
||||
}
|
||||
@@ -119,5 +133,4 @@ PresentPhoto.propTypes = {
|
||||
photo: PropTypes.object,
|
||||
thumbnail: PropTypes.string,
|
||||
imageLoaded: PropTypes.func,
|
||||
photoId: PropTypes.string,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import SidebarItem from './SidebarItem'
|
||||
import { Loader } from 'semantic-ui-react'
|
||||
import ProtectedImage from '../photoGallery/ProtectedImage'
|
||||
import SidebarShare from './Sharing'
|
||||
import { SidebarConsumer } from './Sidebar'
|
||||
|
||||
const photoQuery = gql`
|
||||
query sidebarPhoto($id: ID!) {
|
||||
@@ -61,57 +62,72 @@ const exifNameLookup = {
|
||||
flash: 'Flash',
|
||||
}
|
||||
|
||||
const SidebarContent = ({ photo, hidePreview }) => {
|
||||
let exifItems = []
|
||||
|
||||
if (photo && photo.exif) {
|
||||
let exifKeys = Object.keys(photo.exif).filter(
|
||||
x => !!photo.exif[x] && x != '__typename'
|
||||
)
|
||||
|
||||
let exif = exifKeys.reduce(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr]: photo.exif[curr],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
||||
exif.dateShot = new Date(exif.dateShot.formatted).toLocaleString()
|
||||
|
||||
exifItems = exifKeys.map(key => (
|
||||
<SidebarItem key={key} name={exifNameLookup[key]} value={exif[key]} />
|
||||
))
|
||||
}
|
||||
|
||||
let previewUrl = null
|
||||
if (photo) {
|
||||
if (photo.original) previewUrl = photo.original.url
|
||||
else if (photo.thumbnail) previewUrl = photo.thumbnail.url
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!hidePreview && <PreviewImage src={previewUrl} />}
|
||||
<Name>{photo && photo.title}</Name>
|
||||
<div>{exifItems}</div>
|
||||
<SidebarShare photo={photo} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SidebarContent.propTypes = {
|
||||
photo: PropTypes.object,
|
||||
hidePreview: PropTypes.bool,
|
||||
}
|
||||
|
||||
class PhotoSidebar extends Component {
|
||||
render() {
|
||||
const { imageId } = this.props
|
||||
const { photo, hidePreview } = this.props
|
||||
|
||||
if (!imageId) {
|
||||
return null
|
||||
if (!photo) return null
|
||||
|
||||
if (!localStorage.getItem('token')) {
|
||||
return <SidebarContent photo={photo} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Query query={photoQuery} variables={{ id: imageId }}>
|
||||
<Query query={photoQuery} variables={{ id: photo.id }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (error) return error
|
||||
|
||||
const { photo } = data
|
||||
let exifItems = []
|
||||
|
||||
if (photo && photo.exif) {
|
||||
let exifKeys = Object.keys(photo.exif).filter(
|
||||
x => !!photo.exif[x] && x != '__typename'
|
||||
)
|
||||
|
||||
let exif = exifKeys.reduce(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr]: photo.exif[curr],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
||||
exif.dateShot = new Date(exif.dateShot.formatted).toLocaleString()
|
||||
|
||||
exifItems = exifKeys.map(key => (
|
||||
<SidebarItem
|
||||
key={key}
|
||||
name={exifNameLookup[key]}
|
||||
value={exif[key]}
|
||||
/>
|
||||
))
|
||||
if (loading) {
|
||||
return <SidebarContent photo={photo} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Loader active={loading} />
|
||||
<PreviewImage
|
||||
src={photo && photo.original && photo.original.url}
|
||||
/>
|
||||
<Name>{photo && photo.title}</Name>
|
||||
<div>{exifItems}</div>
|
||||
<SidebarShare photo={photo} />
|
||||
</div>
|
||||
<SidebarContent photo={data.photo} hidePreview={hidePreview} />
|
||||
)
|
||||
}}
|
||||
</Query>
|
||||
@@ -121,7 +137,8 @@ class PhotoSidebar extends Component {
|
||||
}
|
||||
|
||||
PhotoSidebar.propTypes = {
|
||||
imageId: PropTypes.string.isRequired,
|
||||
photo: PropTypes.object.isRequired,
|
||||
hidePreview: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default PhotoSidebar
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import { Query, Mutation } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import { Table, Button, Dropdown } from 'semantic-ui-react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
const sharePhotoQuery = gql`
|
||||
query sidbarGetPhotoShares($id: ID!) {
|
||||
@@ -54,6 +55,7 @@ const deleteShareMutation = gql`
|
||||
|
||||
const SidebarShare = ({ photo, album }) => {
|
||||
if ((!photo || !photo.id) && (!album || !album.id)) return null
|
||||
if (!localStorage.getItem('token')) return null
|
||||
|
||||
const isPhoto = !!photo
|
||||
const id = isPhoto ? photo.id : album.id
|
||||
@@ -80,7 +82,13 @@ const SidebarShare = ({ photo, album }) => {
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button.Group>
|
||||
<Button icon="chain" content="Copy" />
|
||||
<Button
|
||||
icon="chain"
|
||||
content="Copy link"
|
||||
onClick={() => {
|
||||
copy(`${location.origin}/share/${share.token}`)
|
||||
}}
|
||||
/>
|
||||
<Dropdown button text="More">
|
||||
<Dropdown.Menu>
|
||||
<Mutation
|
||||
|
||||
Reference in New Issue
Block a user