diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go index 7ea9284c..f863b514 100644 --- a/api/graphql/resolvers/faces.go +++ b/api/graphql/resolvers/faces.go @@ -150,7 +150,83 @@ func (r *mutationResolver) CombineFaceGroups(ctx context.Context, destinationFac } func (r *mutationResolver) MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error) { - panic("not implemented") + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + if err := user.FillAlbums(r.Database); err != nil { + return nil, err + } + + userAlbumIDs := make([]int, len(user.Albums)) + for i, album := range user.Albums { + userAlbumIDs[i] = album.ID + } + + userOwnedImageFaceIDs := make([]int, 0) + var destFaceGroup *models.FaceGroup + + transErr := r.Database.Transaction(func(tx *gorm.DB) error { + + var err error + destFaceGroup, err = userOwnedFaceGroup(tx, user, destinationFaceGroupID) + if err != nil { + return err + } + + var userOwnedImageFaces []*models.ImageFace + if err := tx. + Joins("JOIN media ON media.id = image_faces.media_id"). + Where("media.album_id IN (?)", userAlbumIDs). + Where("image_faces.id IN (?)", imageFaceIDs). + Find(&userOwnedImageFaces).Error; err != nil { + return err + } + + for _, imageFace := range userOwnedImageFaces { + userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID) + } + + var sourceFaceGroups []*models.FaceGroup + if err := tx. + Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id"). + Where("image_faces.id IN (?)", userOwnedImageFaceIDs). + Find(&sourceFaceGroups).Error; err != nil { + return err + } + + if err := tx. + Model(&models.ImageFace{}). + Where("id IN (?)", userOwnedImageFaceIDs). + Update("face_group_id", destFaceGroup.ID).Error; err != nil { + return err + } + + // delete face groups if they have become empty + for _, faceGroup := range sourceFaceGroups { + var count int64 + if err := tx.Model(&models.ImageFace{}).Where("face_group_id = ?", faceGroup.ID).Count(&count).Error; err != nil { + return err + } + + if count == 0 { + if err := tx.Delete(&faceGroup).Error; err != nil { + return err + } + } + } + + return nil + }) + + if transErr != nil { + return nil, transErr + } + + face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(destFaceGroup.ID)) + + return destFaceGroup, nil } func (r *mutationResolver) RecognizeUnlabeledFaces(ctx context.Context) ([]*models.ImageFace, error) { diff --git a/api/scanner/face_detection/face_detector.go b/api/scanner/face_detection/face_detector.go index da4ab32e..f0ef2cf3 100644 --- a/api/scanner/face_detection/face_detector.go +++ b/api/scanner/face_detection/face_detector.go @@ -12,11 +12,12 @@ import ( ) type FaceDetector struct { - mutex sync.Mutex - db *gorm.DB - rec *face.Recognizer - samples []face.Descriptor - cats []int32 + mutex sync.Mutex + db *gorm.DB + rec *face.Recognizer + faceDescriptors []face.Descriptor + faceGroupIDs []int32 + imageFaceIDs []int } var GlobalFaceDetector FaceDetector @@ -30,22 +31,23 @@ func InitializeFaceDetector(db *gorm.DB) error { return errors.Wrap(err, "initialize facedetect recognizer") } - samples, cats, err := getSamplesFromDatabase(db) + faceDescriptors, faceGroupIDs, imageFaceIDs, err := getSamplesFromDatabase(db) if err != nil { return errors.Wrap(err, "get face detection samples from database") } GlobalFaceDetector = FaceDetector{ - db: db, - rec: rec, - samples: samples, - cats: cats, + db: db, + rec: rec, + faceDescriptors: faceDescriptors, + faceGroupIDs: faceGroupIDs, + imageFaceIDs: imageFaceIDs, } return nil } -func getSamplesFromDatabase(db *gorm.DB) (samples []face.Descriptor, cats []int32, err error) { +func getSamplesFromDatabase(db *gorm.DB) (samples []face.Descriptor, faceGroupIDs []int32, imageFaceIDs []int, err error) { var imageFaces []*models.ImageFace @@ -54,11 +56,13 @@ func getSamplesFromDatabase(db *gorm.DB) (samples []face.Descriptor, cats []int3 } samples = make([]face.Descriptor, len(imageFaces)) - cats = make([]int32, len(imageFaces)) + faceGroupIDs = make([]int32, len(imageFaces)) + imageFaceIDs = make([]int, len(imageFaces)) for i, imgFace := range imageFaces { samples[i] = face.Descriptor(imgFace.Descriptor) - cats[i] = int32(imgFace.FaceGroupID) + faceGroupIDs[i] = int32(imgFace.FaceGroupID) + imageFaceIDs[i] = imgFace.ID } return @@ -150,10 +154,11 @@ func (fd *FaceDetector) classifyFace(face *face.Face, media *models.Media, image } } - fd.samples = append(fd.samples, face.Descriptor) - fd.cats = append(fd.cats, int32(faceGroup.ID)) + fd.faceDescriptors = append(fd.faceDescriptors, face.Descriptor) + fd.faceGroupIDs = append(fd.faceGroupIDs, int32(faceGroup.ID)) + fd.imageFaceIDs = append(fd.imageFaceIDs, imageFace.ID) - fd.rec.SetSamples(fd.samples, fd.cats) + fd.rec.SetSamples(fd.faceDescriptors, fd.faceGroupIDs) return nil } @@ -161,19 +166,37 @@ func (fd *FaceDetector) MergeCategories(sourceID int32, destID int32) { fd.mutex.Lock() defer fd.mutex.Unlock() - for i := range fd.cats { - if fd.cats[i] == sourceID { - fd.cats[i] = destID + for i := range fd.faceGroupIDs { + if fd.faceGroupIDs[i] == sourceID { + fd.faceGroupIDs[i] = destID + } + } +} + +func (fd *FaceDetector) MergeImageFaces(imageFaceIDs []int, destFaceGroupID int32) { + fd.mutex.Lock() + defer fd.mutex.Unlock() + + for i := range fd.faceGroupIDs { + imageFaceID := fd.imageFaceIDs[i] + + for _, id := range imageFaceIDs { + if imageFaceID == id { + fd.faceGroupIDs[i] = destFaceGroupID + break + } } } } func (fd *FaceDetector) RecognizeUnlabeledFaces(tx *gorm.DB, user *models.User) ([]*models.ImageFace, error) { - unrecognizedSamples := make([]face.Descriptor, 0) - unrecognizedCats := make([]int32, 0) + unrecognizedDescriptors := make([]face.Descriptor, 0) + unrecognizedFaceGroupIDs := make([]int32, 0) + unrecognizedImageFaceIDs := make([]int, 0) - newCats := make([]int32, 0) - newSamples := make([]face.Descriptor, 0) + newFaceGroupIDs := make([]int32, 0) + newDescriptors := make([]face.Descriptor, 0) + newImageFaceIDs := make([]int, 0) var unlabeledFaceGroups []*models.FaceGroup @@ -193,59 +216,64 @@ func (fd *FaceDetector) RecognizeUnlabeledFaces(tx *gorm.DB, user *models.User) fd.mutex.Lock() defer fd.mutex.Unlock() - for i := range fd.samples { - cat := fd.cats[i] - sample := fd.samples[i] + for i := range fd.faceDescriptors { + descriptor := fd.faceDescriptors[i] + faceGroupID := fd.faceGroupIDs[i] + imageFaceID := fd.imageFaceIDs[i] - catIsUnlabeled := false + isUnlabeled := false for _, unlabeledFaceGroup := range unlabeledFaceGroups { - if cat == int32(unlabeledFaceGroup.ID) { - catIsUnlabeled = true + if faceGroupID == int32(unlabeledFaceGroup.ID) { + isUnlabeled = true continue } } - if catIsUnlabeled { - unrecognizedCats = append(unrecognizedCats, cat) - unrecognizedSamples = append(unrecognizedSamples, sample) + if isUnlabeled { + unrecognizedFaceGroupIDs = append(unrecognizedFaceGroupIDs, faceGroupID) + unrecognizedDescriptors = append(unrecognizedDescriptors, descriptor) + unrecognizedImageFaceIDs = append(unrecognizedImageFaceIDs, imageFaceID) } else { - newCats = append(newCats, cat) - newSamples = append(newSamples, sample) + newFaceGroupIDs = append(newFaceGroupIDs, faceGroupID) + newDescriptors = append(newDescriptors, descriptor) + newImageFaceIDs = append(newImageFaceIDs, imageFaceID) } } - fd.cats = newCats - fd.samples = newSamples + fd.faceGroupIDs = newFaceGroupIDs + fd.faceDescriptors = newDescriptors + fd.imageFaceIDs = newImageFaceIDs updatedImageFaces := make([]*models.ImageFace, 0) - for i := range unrecognizedSamples { - cat := unrecognizedCats[i] - sample := unrecognizedSamples[i] + for i := range unrecognizedDescriptors { + descriptor := unrecognizedDescriptors[i] + faceGroupID := unrecognizedFaceGroupIDs[i] + imageFaceID := unrecognizedImageFaceIDs[i] - match := fd.classifyDescriptor(sample) + match := fd.classifyDescriptor(descriptor) if match < 0 { // still no match, we can readd it to the list - fd.cats = append(fd.cats, cat) - fd.samples = append(fd.samples, sample) + fd.faceGroupIDs = append(fd.faceGroupIDs, faceGroupID) + fd.faceDescriptors = append(fd.faceDescriptors, descriptor) + fd.imageFaceIDs = append(fd.imageFaceIDs, imageFaceID) } else { // found new match, update the database var imageFace models.ImageFace - if err := tx.Model(&models.ImageFace{ - Descriptor: models.FaceDescriptor(sample), - }).First(imageFace).Error; err != nil { + if err := tx.Model(&models.ImageFace{}).First(imageFace, imageFaceID).Error; err != nil { return nil, err } - if err := tx.Model(&imageFace).Update("face_group_id", int(cat)).Error; err != nil { + if err := tx.Model(&imageFace).Update("face_group_id", int(faceGroupID)).Error; err != nil { return nil, err } updatedImageFaces = append(updatedImageFaces, &imageFace) - fd.cats = append(fd.cats, match) - fd.samples = append(fd.samples, sample) + fd.faceGroupIDs = append(fd.faceGroupIDs, match) + fd.faceDescriptors = append(fd.faceDescriptors, descriptor) + fd.imageFaceIDs = append(fd.imageFaceIDs, imageFaceID) } } diff --git a/api/server.go b/api/server.go index 8a9adaf2..67f0cd83 100644 --- a/api/server.go +++ b/api/server.go @@ -4,6 +4,7 @@ import ( "log" "net/http" "path" + "time" "github.com/gorilla/handlers" "github.com/gorilla/mux" @@ -88,6 +89,7 @@ func main() { handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig), handler.IntrospectionEnabled(devMode), handler.WebsocketUpgrader(server.WebsocketUpgrader(devMode)), + handler.WebsocketKeepAliveDuration(time.Second*10), handler.WebsocketInitFunc(auth.AuthWebsocketInit(db)), ), ) diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.js b/ui/src/Pages/PeoplePage/FaceCircleImage.js index 4df50d57..d77dc148 100644 --- a/ui/src/Pages/PeoplePage/FaceCircleImage.js +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.js @@ -63,6 +63,10 @@ const CircleImageWrapper = styled.div` ` const FaceCircleImage = ({ imageFace, selectable, size = '150px' }) => { + if (!imageFace) { + return null + } + const rect = imageFace.rectangle let scale = Math.min(1 / (rect.maxX - rect.minX), 1 / (rect.maxY - rect.minY)) @@ -89,7 +93,7 @@ const FaceCircleImage = ({ imageFace, selectable, size = '150px' }) => { } FaceCircleImage.propTypes = { - imageFace: PropTypes.object.isRequired, + imageFace: PropTypes.object, selectable: PropTypes.bool, size: PropTypes.string, } diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.js index f445a5f6..f4476d8a 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.js +++ b/ui/src/Pages/PeoplePage/PeoplePage.js @@ -24,6 +24,7 @@ export const MY_FACES_QUERY = gql` media { id type + title thumbnail { url width diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js index 6836b2b8..b399db05 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js @@ -5,6 +5,7 @@ import { Dropdown, Input } from 'semantic-ui-react' import styled from 'styled-components' import { SET_GROUP_LABEL_MUTATION } from '../PeoplePage' import MergeFaceGroupsModal from './MergeFaceGroupsModal' +import MoveImageFacesModal from './MoveImageFacesModal' const TitleWrapper = styled.div` min-height: 3.5em; @@ -31,6 +32,7 @@ const FaceGroupTitle = ({ faceGroup }) => { const [inputValue, setInputValue] = useState(faceGroup?.label ?? '') const inputRef = createRef() const [mergeModalOpen, setMergeModalOpen] = useState(false) + const [moveModalOpen, setMoveModalOpen] = useState(false) const [setGroupLabel, { loading: setLabelLoading }] = useMutation( SET_GROUP_LABEL_MUTATION, @@ -94,11 +96,20 @@ const FaceGroupTitle = ({ faceGroup }) => { onClick={() => setEditLabel(true)} /> setMergeModalOpen(true)} /> + setMergeModalOpen(true)} + /> + setMoveModalOpen(true)} + /> @@ -130,6 +141,11 @@ const FaceGroupTitle = ({ faceGroup }) => { setOpen={setMergeModalOpen} sourceFaceGroup={faceGroup} /> + ) } diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js index c38daaf6..9d7dff24 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js @@ -1,18 +1,10 @@ -import React, { useState } from 'react' -import PropTypes from 'prop-types' -import { - Modal, - Button, - Header, - Table, - Input, - Pagination, -} from 'semantic-ui-react' -import FaceCircleImage from '../FaceCircleImage' import { gql, useMutation, useQuery } from '@apollo/client' -import { MY_FACES_QUERY } from '../PeoplePage' -import styled from 'styled-components' +import PropTypes from 'prop-types' +import React, { useState } from 'react' import { useHistory } from 'react-router-dom' +import { Button, Modal } from 'semantic-ui-react' +import { MY_FACES_QUERY } from '../PeoplePage' +import SelectFaceGroupTable from './SelectFaceGroupTable' const COMBINE_FACES_MUTATION = gql` mutation($destID: ID!, $srcID: ID!) { @@ -25,53 +17,9 @@ const COMBINE_FACES_MUTATION = gql` } ` -const FaceCircleWrapper = styled.div` - display: inline-block; - border-radius: 50%; - border: 2px solid - ${({ $selected }) => ($selected ? `#2185c9` : 'rgba(255,255,255,0)')}; -` - -const FaceGroupRowStyled = styled(Table.Row)`` - -const FaceGroupCell = styled(Table.Cell)` - display: flex; - align-items: center; -` - -const RowLabel = styled.span` - ${({ $selected }) => $selected && `font-weight: bold;`} - margin-left: 12px; -` - -const FaceGroupRow = ({ faceGroup, faceSelected, setFaceSelected }) => { - return ( - - - - - - {faceGroup.label} - - - ) -} - -FaceGroupRow.propTypes = { - faceGroup: PropTypes.object.isRequired, - faceSelected: PropTypes.bool.isRequired, - setFaceSelected: PropTypes.func.isRequired, -} - const MergeFaceGroupsModal = ({ open, setOpen, sourceFaceGroup }) => { - const [page, setPage] = useState(0) - const [searchValue, setSearchValue] = useState('') - const [selectedRow, setSelectedRow] = useState(null) - const PAGE_SIZE = 6 + const [selectedFaceGroup, setSelectedFaceGroup] = useState(null) + let history = useHistory() const { data } = useQuery(MY_FACES_QUERY) const [combineFacesMutation] = useMutation(COMBINE_FACES_MUTATION, { @@ -88,40 +36,19 @@ const MergeFaceGroupsModal = ({ open, setOpen, sourceFaceGroup }) => { if (open == false) return null const filteredFaceGroups = - data?.myFaceGroups - .filter( - x => - searchValue == '' || - (x.label && x.label.toLowerCase().includes(searchValue.toLowerCase())) - ) - .filter(x => x.id != sourceFaceGroup?.id) ?? [] - - console.log(filteredFaceGroups) + data?.myFaceGroups.filter(x => x.id != sourceFaceGroup?.id) ?? [] const mergeFaceGroups = () => { - const destFaceGroup = filteredFaceGroups[selectedRow] - combineFacesMutation({ variables: { - destID: destFaceGroup.id, + destID: selectedFaceGroup.id, }, }).then(() => { setOpen(false) - history.push(`/people/${destFaceGroup.id}`) + history.push(`/people/${selectedFaceGroup.id}`) }) } - const rows = filteredFaceGroups - .filter((_, i) => i >= page * PAGE_SIZE && i < (page + 1) * PAGE_SIZE) - .map((face, i) => ( - setSelectedRow(i + page * PAGE_SIZE)} - /> - )) - return ( setOpen(false)} @@ -129,52 +56,24 @@ const MergeFaceGroupsModal = ({ open, setOpen, sourceFaceGroup }) => { open={open} > Merge Face Groups - + -
Select the destination face below
- - - - Face group - - - - setSearchValue(e.target.value)} - icon="search" - placeholder="Search faces..." - fluid - /> - - - - {rows} - - - - { - setPage(Math.ceil(activePage) - 1) - }} - /> - - - -
+

+ All images within this face group will be merged into the selected + face group. +

+
+ {positiveButton} + +
+ ) +} + +MoveImageFacesModal.propTypes = { + open: PropTypes.bool.isRequired, + setOpen: PropTypes.func.isRequired, + faceGroup: PropTypes.object, +} + +export default MoveImageFacesModal diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js new file mode 100644 index 00000000..e8594dd4 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js @@ -0,0 +1,125 @@ +import PropTypes from 'prop-types' +import React, { useState, useEffect } from 'react' +import { Input, Pagination, Table } from 'semantic-ui-react' +import styled from 'styled-components' +import FaceCircleImage from '../FaceCircleImage' + +const FaceCircleWrapper = styled.div` + display: inline-block; + border-radius: 50%; + border: 2px solid + ${({ $selected }) => ($selected ? `#2185c9` : 'rgba(255,255,255,0)')}; +` + +const FlexCell = styled(Table.Cell)` + display: flex; + align-items: center; +` + +export const RowLabel = styled.span` + ${({ $selected }) => $selected && `font-weight: bold;`} + margin-left: 12px; +` + +const FaceGroupRow = ({ faceGroup, faceSelected, setFaceSelected }) => { + return ( + + + + + + {faceGroup.label} + + + ) +} + +FaceGroupRow.propTypes = { + faceGroup: PropTypes.object.isRequired, + faceSelected: PropTypes.bool.isRequired, + setFaceSelected: PropTypes.func.isRequired, +} + +const SelectFaceGroupTable = ({ + faceGroups, + selectedFaceGroup, + setSelectedFaceGroup, + title, +}) => { + const PAGE_SIZE = 6 + + const [page, setPage] = useState(0) + const [searchValue, setSearchValue] = useState('') + + useEffect(() => { + setPage(0) + }, [searchValue]) + + const rows = faceGroups + .filter( + x => + searchValue == '' || + (x.label && x.label.toLowerCase().includes(searchValue.toLowerCase())) + ) + .map(face => ( + setSelectedFaceGroup(face)} + /> + )) + + const pageRows = rows.filter( + (_, i) => i >= page * PAGE_SIZE && i < (page + 1) * PAGE_SIZE + ) + + return ( + + + + {title} + + + + setSearchValue(e.target.value)} + icon="search" + placeholder="Search faces..." + fluid + /> + + + + {pageRows} + + + + { + setPage(Math.ceil(activePage) - 1) + }} + /> + + + +
+ ) +} + +SelectFaceGroupTable.propTypes = { + faceGroups: PropTypes.array, + selectedFaceGroup: PropTypes.object, + setSelectedFaceGroup: PropTypes.func.isRequired, + title: PropTypes.string, +} + +export default SelectFaceGroupTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js new file mode 100644 index 00000000..c7d7abf0 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types' +import React, { useEffect, useState } from 'react' +import { Checkbox, Input, Pagination, Table } from 'semantic-ui-react' +import styled from 'styled-components' +import { ProtectedImage } from '../../../components/photoGallery/ProtectedMedia' +import { RowLabel } from './SelectFaceGroupTable' + +const SelectImagePreview = styled(ProtectedImage)` + max-width: 120px; + max-height: 80px; +` + +const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { + return ( + + + + + + + + + + {imageFace.media.title} + + + + ) +} + +ImageFaceRow.propTypes = { + imageFace: PropTypes.object.isRequired, + faceSelected: PropTypes.bool.isRequired, + setFaceSelected: PropTypes.func.isRequired, +} + +const SelectImageFacesTable = ({ + imageFaces, + selectedImageFaces, + setSelectedImageFaces, + title, +}) => { + const PAGE_SIZE = 6 + + const [page, setPage] = useState(0) + const [searchValue, setSearchValue] = useState('') + + useEffect(() => { + setPage(0) + }, [searchValue]) + + const rows = imageFaces + .filter( + face => + searchValue == '' || + face.media.title.toLowerCase().includes(searchValue.toLowerCase()) + ) + .map(face => ( + + setSelectedImageFaces(faces => { + if (faces.includes(face)) { + return faces.filter(x => x != face) + } else { + return [...faces, face] + } + }) + } + /> + )) + + const pageRows = rows.filter( + (_, i) => i >= page * PAGE_SIZE && i < (page + 1) * PAGE_SIZE + ) + + return ( + + + + {title} + + + + setSearchValue(e.target.value)} + icon="search" + placeholder="Search images..." + fluid + /> + + + + {pageRows} + + + + { + setPage(Math.ceil(activePage) - 1) + }} + /> + + + +
+ ) +} + +SelectImageFacesTable.propTypes = { + imageFaces: PropTypes.array, + selectedImageFaces: PropTypes.array, + setSelectedImageFaces: PropTypes.func.isRequired, + title: PropTypes.string, +} + +export default SelectImageFacesTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js index a6110817..c2846e2d 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js @@ -1,22 +1,8 @@ import PropTypes from 'prop-types' import React, { useState } from 'react' import PhotoGallery from '../../../components/photoGallery/PhotoGallery' -import { ProtectedImage } from '../../../components/photoGallery/ProtectedMedia' import FaceGroupTitle from './FaceGroupTitle' -const ImageFace = ({ imageFace }) => { - return ( -
- Image face: {imageFace.id} - -
- ) -} - -ImageFace.propTypes = { - imageFace: PropTypes.object.isRequired, -} - const SingleFaceGroup = ({ faceGroup }) => { const [presenting, setPresenting] = useState(false) const [activeIndex, setActiveIndex] = useState(-1)